antisketch 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Harsh Joshi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -4,6 +4,8 @@ Turn websites you like into design skills for your coding agent.
4
4
 
5
5
  Paste a link. Antisketch studies the site's typography, colors, spacing, and interactions, then builds two demos that use those ideas for different tasks. Review the results, ask for changes, and save the version you like as a skill.
6
6
 
7
+ ![Antisketch demo: paste a reference URL, review the captured design, try two generated demos, and approve one as a skill](https://josharsh.github.io/Josharsh/assets/antisketch/demo.gif)
8
+
7
9
  The aim is to give your agent a useful understanding of a design: what gives it character, how the pieces work together, and how to apply it somewhere else.
8
10
 
9
11
  ## Install
@@ -27,7 +29,7 @@ Choose **Connect Codex** and sign in through the browser. If you're already sign
27
29
  4. Ask for another revision if something feels off.
28
30
  5. Pick a demo and approve the design when you're happy with it.
29
31
 
30
- Preparation has several passes: capture desktop and mobile sections, write two distinct concept briefs, create original SVG artwork, build each demo, then render and test it. The two experiments build concurrently, with artwork, HTML/CSS, and JavaScript generated in separate passes to keep each response manageable. Codex compares the screenshots with the reference and gets one refinement pass when the review finds issues. This takes longer and uses more of your Codex allowance than a single generation. The visual review is visible beside the experiments; it helps you judge the result, but it isn't a quality guarantee.
32
+ Preparation has several passes: capture desktop and mobile sections, write two distinct concept briefs, create original SVG artwork, build each demo, then render and test it. The two experiments build concurrently, with artwork, HTML/CSS, and JavaScript generated in separate passes to keep each response manageable. Codex compares the screenshots with the reference and gets one refinement pass when the review finds issues; experiments that need it are refined concurrently too, and a step that stalls is retried once automatically. This takes longer and uses more of your Codex allowance than a single generation. The visual review is visible beside the experiments; it helps you judge the result, but it isn't a quality guarantee.
31
33
 
32
34
  You decide what gets approved. An approved skill includes the design spec, tokens, supporting evidence, and your chosen demo. Earlier approvals stay available when you make a new revision.
33
35
 
package/app/public/app.js CHANGED
@@ -2,7 +2,7 @@
2
2
  const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)];
3
3
  const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
4
4
  const active=['queued','capturing','interpreting','validating'];
5
- let session=null,designs=[],releases=[],detail=null,tab='reference',selectedDemo=1,device='desktop',screenshot=0,revision=null,search='',routeGeneration=0,polling=false;
5
+ let session=null,designs=[],releases=[],detail=null,tab='reference',selectedDemo=1,device='desktop',screenshot=0,revision=null,search='',routeGeneration=0,polling=false,elapsedTimer=null;
6
6
  let pollFingerprint='',listFingerprint='',resizeObserver,approvalTarget=null;
7
7
  const main=$('#main');
8
8
  async function api(url,options={}){const response=await fetch(url,{...options,headers:{'Content-Type':'application/json',...(session?{'X-Antisketch-Token':session.token}:{}),...options.headers}});const result=await response.json();if(!response.ok){const error=new Error(result.error||'Unable to complete this action');error.id=result.id;throw error;}return result;}
@@ -30,6 +30,7 @@ function renderList(mode){main.className='collection-page';listFingerprint=JSON.
30
30
  }
31
31
  function renderDetail(){main.className='detail-page';const d=detail,r=d.selectedRevision;renderNav(r.status==='approved'?'library':d.archived?'archive':'inbox');$('#breadcrumb').textContent='Your workspace / '+d.name;const editable=r.number===d.current_revision;const busy=active.includes(r.status);const release=d.releases.find(x=>x.revision===r.number);
32
32
  main.innerHTML=`<div class="meta-row"><a href="#inbox">← Your desk</a>${badge(r.status)}<label>Revision <select id="revision-select" aria-label="View revision">${d.revisions.map(v=>`<option value="${v.number}" ${v.number===r.number?'selected':''}>${v.number} · ${esc(v.status)}</option>`).join('')}</select></label><a href="${esc(d.url)}" target="_blank" rel="noopener noreferrer">${esc(new URL(d.url).hostname)} ↗</a></div><div class="detail-head"><div><h1>${esc(d.summary?.name||d.name)}</h1><p>${esc(d.summary?.summary||'We’re gathering the details that give this reference its character. You can leave this screen; preparation continues while the app is running.')}</p></div><div class="detail-actions">${release?`<button class="button small" data-use-release="${release.id}">Use in… ↗</button><a class="button secondary small" href="/api/releases/${release.id}/download">Download skill ↓</a>`:''}${!busy&&editable?`<button class="button secondary small" id="archive-button">${d.archived?'Restore to inbox':'Archive'}</button>`:''}</div></div>${!editable?'<div class="notice">You’re looking at a previous revision. Its record is preserved; changes and approval happen on the latest revision.</div>':''}${r.status==='approved'?`<div class="notice">Version ${release?.version||''} is approved and frozen. Start a new revision below whenever your taste evolves.</div>`:''}${busy?progressHTML(r):r.status==='failed'||r.status==='cancelled'?`<div class="progress-card"><span class="eyebrow">${r.status==='failed'?'NEEDS A LITTLE ATTENTION':'STOPPED FOR NOW'}</span><h2>${r.status==='failed'?'Let’s give this another look.':'Your work is still here.'}</h2><p class="muted">${esc(r.error||r.stage)}</p><button class="button" id="retry-button">Retry extraction ↗</button>${d.capture?'<p class="fine">Captured evidence is retained below, even though this revision is incomplete.</p>':''}</div>`:''}${d.summary||d.capture?`<div class="tabs" role="tablist" aria-label="Review sections">${[['reference','01','Reference'],['language','02','Design language'],['experiments','03','Experiments'],['review','04','Review & keep']].map(([id,n,label])=>`<button role="tab" id="tab-${id}" aria-controls="tab-content" aria-selected="${tab===id}" tabindex="${tab===id?'0':'-1'}" data-tab="${id}" class="${tab===id?'active':''}"><span>${n}</span>${label}</button>`).join('')}</div><section id="tab-content" role="tabpanel" aria-labelledby="tab-${tab}"></section>`:''}`;
33
+ clearInterval(elapsedTimer);if(busy){const since=Date.parse(r.updated_at);const tick=()=>{const el=$('#elapsed');if(!el)return;const s=Math.max(0,Math.round((Date.now()-since)/1000));el.textContent=`${Math.floor(s/60)}:${String(s%60).padStart(2,'0')}`;};tick();elapsedTimer=setInterval(tick,1000);}
33
34
  $('#revision-select').addEventListener('change',e=>{location.hash=`design/${d.id}/${e.target.value}`;});
34
35
  $('#archive-button')?.addEventListener('click',()=>mutate(async()=>{await api(`/api/designs/${d.id}/archive`,{method:'POST',body:JSON.stringify({archived:!d.archived})});toast(d.archived?'Back on your desk':'Reference archived');await refreshLists();await loadDetail(d.id,r.number); }));
35
36
  $('#cancel-button')?.addEventListener('click',()=>mutate(async()=>{await api(`/api/designs/${d.id}/cancel`,{method:'POST',body:'{}'});await loadDetail(d.id,r.number);}));
@@ -38,7 +39,7 @@ function renderDetail(){main.className='detail-page';const d=detail,r=d.selected
38
39
  $('.tabs')?.addEventListener('keydown',e=>{if(!['ArrowLeft','ArrowRight','Home','End'].includes(e.key))return;e.preventDefault();const tabs=$$('[data-tab]');let i=tabs.indexOf(document.activeElement);i=e.key==='Home'?0:e.key==='End'?tabs.length-1:(i+(e.key==='ArrowRight'?1:-1)+tabs.length)%tabs.length;tabs[i].click();});
39
40
  renderTab();
40
41
  }
41
- function progressHTML(r){const stages=['queued','capturing','interpreting','validating'],current=stages.indexOf(r.status);return `<div class="progress-card"><div class="spinner" aria-hidden="true"></div><h2>${esc(r.stage)}</h2><p class="muted">We’re preparing an evidence-backed design study and two working applications. This usually takes several minutes. You can keep browsing your collection.</p><div class="steps">${['Queued','Capture','Interpret & create','Check'].map((x,i)=>`<span class="step ${i<current?'done':i===current?'current':''}">${i<current?'✓':String(i+1).padStart(2,'0')} ${x}</span>`).join('')}</div><button class="button secondary small" id="cancel-button">Cancel preparation</button></div>`;}
42
+ function progressHTML(r){const stages=['queued','capturing','interpreting','validating'],current=stages.indexOf(r.status);return `<div class="progress-card"><div class="spinner" aria-hidden="true"></div><h2>${esc(r.stage)} <span class="elapsed" id="elapsed"></span></h2><p class="muted">We’re preparing an evidence-backed design study and two working applications. This usually takes about ten minutes; each step takes a minute or two. You can keep browsing your collection.</p><div class="steps">${['Queued','Capture','Interpret & create','Check'].map((x,i)=>`<span class="step ${i<current?'done':i===current?'current':''}">${i<current?'✓':String(i+1).padStart(2,'0')} ${x}</span>`).join('')}</div><button class="button secondary small" id="cancel-button">Cancel preparation</button></div>`;}
42
43
  function renderTab(){resizeObserver?.disconnect();const root=$('#tab-content');if(!root)return;const d=detail,r=d.selectedRevision;
43
44
  if(tab==='reference'){const c=d.capture;if(!c){root.innerHTML='<p class="muted">Reference captures will appear here when inspection finishes.</p>';return;}screenshot=Math.min(screenshot,c.screenshots.length-1);const s=c.screenshots[screenshot];root.innerHTML=`<div class="split"><div class="panel"><div class="panel-top"><select id="capture-select" aria-label="Reference capture">${c.screenshots.map((s,i)=>`<option value="${i}" ${i===screenshot?'selected':''}>${esc(s.label)}</option>`).join('')}</select><span>${s?.viewport?esc(s.viewport.width+' × '+s.viewport.height):'CAPTURED REFERENCE'}</span></div>${s?`<a href="${file(s.file)}" target="_blank" rel="noopener"><img class="reference-image" src="${file(s.file)}" alt="${esc(s.label)}"></a>`:'<p>No screenshot available.</p>'}</div><aside><div class="side-note"><span class="eyebrow">WHAT WE LOOKED AT</span><h3 style="margin-top:15px">Captured ${date(c.capturedAt)}</h3><span class="metric">${c.coverage.routes.length} <small style="font:12px var(--sans)">pages sampled</small></span><p>${c.coverage.viewports.map(esc).join('<br>')}</p></div><div class="side-note"><h3>States inspected</h3>${c.coverage.states.map(x=>`<span class="scope-pill">${esc(x)}</span>`).join('')}</div><div class="side-note"><h3>Know the edges</h3><ul>${[...c.warnings,...c.coverage.limits].map(x=>`<li>${esc(x)}</li>`).join('')}</ul></div><p class="fine">Source imagery is reference material. Approval does not grant permission to reuse its photos, logos, or fonts.</p></aside></div>`;$('#capture-select').addEventListener('change',e=>{screenshot=Number(e.target.value);renderTab();});return;}
44
45
  if(!d.summary){root.innerHTML='<div class="notice">The design interpretation is not ready yet. Check the reference captures or retry preparation.</div>';return;}
@@ -314,3 +314,5 @@ button.connection{border:0;background:none;padding:6px 0;font:9px var(--mono);co
314
314
  #settings-dialog{max-width:620px}#settings-dialog h2{font-size:42px;line-height:1.05;letter-spacing:-1.7px}
315
315
  .setup-intro{color:var(--muted);line-height:1.7;margin:18px 0 28px}.setup-connection{border-top:1px solid var(--ink);padding-top:20px}.setup-step{display:flex;justify-content:space-between;gap:20px;font:9px var(--mono);letter-spacing:.04em;color:var(--muted)}.setup-state{text-transform:uppercase}.setup-state.ready{color:var(--ink)}.setup-connection h3{font:500 25px var(--display);letter-spacing:-.7px;margin:22px 0 12px}.setup-account{font-size:13px;margin-bottom:12px;overflow-wrap:anywhere}.setup-account span{color:var(--muted)}.setup-connection>.muted{font-size:12px;line-height:1.7}.setup-actions{display:flex;flex-wrap:wrap;align-items:center;gap:18px;margin:24px 0}.setup-actions .fine{flex-basis:100%;margin:0}.setup-browser{border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:20px 0}.setup-browser p{font-size:12px;line-height:1.7;margin:13px 0 0}.setup-browser .text-button,.setup-browser .link-button{margin:15px 20px 0 0}.setup-data-note{margin:20px 0}.setup-storage{margin-top:24px;border-top:1px solid var(--line);padding-top:20px;font-size:12px}.setup-storage summary{cursor:pointer;font:10px var(--mono)}.setup-storage p{margin:18px 0 8px}.setup-storage code{display:block;overflow-wrap:anywhere;font-size:10px}.setup-storage a{display:inline-block;margin-top:15px}#setup-error:empty{display:none}#setup-error{margin:15px 0}
316
316
  @media(max-width:600px){#settings-dialog h2{font-size:34px}.setup-connection h3{font-size:23px}.setup-actions .button{width:100%}.setup-step{font-size:8px;gap:10px}}
317
+
318
+ .elapsed{font:13px var(--mono,ui-monospace,monospace);color:var(--muted,#6b7280);font-variant-numeric:tabular-nums;font-weight:400;vertical-align:middle}
@@ -6,7 +6,11 @@ import os from 'node:os';
6
6
  import {z} from 'zod';
7
7
  import {atomic,readJSON} from './files.js';
8
8
  function providerError(message){return String(message||'Provider reported an error').replace(/https?:\/\/\S+/g,'[URL]').replace(/(?:Bearer\s+\S+|sk-[\w-]+|eyJ[\w.-]+)/gi,'[redacted]').slice(0,400);}
9
- export async function runPhase({schema,prompt,images=[],dir,phase,effort=phase==='design-direction'?'high':'medium',model=process.env.ANTISKETCH_MODEL||null,signal,onProgress=()=>{},label=phase}){
9
+ // A phase asks for a few thousand output tokens and normally finishes within two minutes. One that runs
10
+ // several times longer is a stalled stream rather than slow work, so it is killed and tried once more.
11
+ // Errors marked retry:false are deterministic (output limit, auth, quota, cancel) and fail immediately.
12
+ const failure=(message,retry)=>Object.assign(new Error(message),{retry});
13
+ export async function runPhase({schema,prompt,images=[],dir,phase,effort=phase==='design-direction'?'high':'medium',model=process.env.ANTISKETCH_MODEL||null,signal,onProgress=()=>{},label=phase,deadlineMs=(phase==='design-direction'?8:5)*60*1000}){
10
14
  signal?.throwIfAborted();
11
15
  const temp=fs.mkdtempSync(path.join(os.tmpdir(),'antisketch-agent-'));
12
16
  try{
@@ -15,20 +19,26 @@ export async function runPhase({schema,prompt,images=[],dir,phase,effort=phase==
15
19
  const args=['exec','--ignore-user-config','--ephemeral','--skip-git-repo-check','--sandbox','read-only','-c',`model_reasoning_effort="${effort}"`,'-c','features.shell_tool=false','-c','web_search="disabled"','-c','project_doc_max_bytes=0','--output-schema',schemaPath,'--output-last-message',output,'--color','never','--json','-C',temp];
16
20
  for(const file of images)if(fs.existsSync(file))args.push('--image',file);
17
21
  if(model)args.push('--model',model);
18
- args.push('-');onProgress(label);
19
- const started=Date.now();const diagnostics={phase,model:model||'Codex default (unresolved)',reasoningEffort:effort,startedAt:new Date().toISOString(),events:0,lastEvent:null,lastItem:null,usage:null};await new Promise((resolve,reject)=>{
20
- const env=codexEnvironment();const executable=codexCommand();
21
- const child=spawn(executable.command,[...executable.prefix,...args],{env,stdio:['pipe','pipe','pipe'],detached:process.platform!=='win32'});let stderr='',bytes=0,finished=false,pending='';
22
- const heartbeat=setInterval(()=>{atomic(path.join(dir,`phase-${phase}.json`),{...diagnostics,durationMs:Date.now()-started,running:true});onProgress(`${label} · ${Math.max(1,Math.floor((Date.now()-started)/60000))} min`);},60000);heartbeat.unref();
23
- const stop=()=>{try{if(process.platform!=='win32')process.kill(-child.pid,'SIGTERM');else child.kill('SIGTERM');}catch{}setTimeout(()=>{try{if(process.platform!=='win32')process.kill(-child.pid,'SIGKILL');else child.kill('SIGKILL');}catch{}},1500).unref();};
24
- const abort=()=>{stop();done(new Error('Preparation cancelled'));};const timer=setTimeout(()=>{stop();done(new Error(`${label} timed out after 12 minutes.${diagnostics.lastProviderError?' Last provider error: '+diagnostics.lastProviderError:' Retry this revision.'}`));},12*60*1000);
25
- function done(error){if(finished)return;finished=true;atomic(path.join(dir,`phase-${phase}.json`),{...diagnostics,durationMs:Date.now()-started,error:error?.message||null});clearTimeout(timer);clearInterval(heartbeat);signal?.removeEventListener('abort',abort);error?reject(error):resolve();}
26
- signal?.addEventListener('abort',abort,{once:true});if(signal?.aborted){abort();return;}
27
- child.stdout.on('data',chunk=>{pending+=chunk.toString();const lines=pending.split('\n');pending=lines.pop();for(const line of lines){try{const event=JSON.parse(line);diagnostics.events++;diagnostics.lastEvent=event.type;if(event.item?.type)diagnostics.lastItem=event.item.type;if(event.usage)diagnostics.usage=event.usage;if(event.type==='error'||event.type==='turn.failed'){diagnostics.lastProviderError=providerError(event.message||event.error?.message);onProgress(`${label} · Codex reported an error${event.type==='error'?'; waiting for retry':''}`);atomic(path.join(dir,`phase-${phase}.json`),{...diagnostics,durationMs:Date.now()-started,running:true});if(event.type==='turn.failed'||/max_output_tokens/.test(diagnostics.lastProviderError)){stop();done(new Error('Codex could not complete '+label+': '+diagnostics.lastProviderError));}}}catch{}}bytes+=chunk.length;if(bytes>5_000_000){stop();done(new Error('Interpretation output exceeded its size limit'));}});
28
- child.stderr.on('data',chunk=>{stderr=(stderr+chunk.toString()).slice(-6000);});child.on('error',e=>done(new Error(e.code==='ENOENT'?'Codex CLI is not installed. Open Studio settings to reconnect.':e.message)));child.on('close',code=>done(code===0?null:new Error(/rate.limit|usage.limit|quota/i.test(stderr)?'Codex usage limit reached. Retry after your allowance resets.':/auth|login|unauthorized|401/i.test(stderr)?'Codex needs authentication. Open Studio settings to reconnect, then retry.':`Codex could not complete this revision (exit ${code}). Check codex login status and retry.`)));
29
- child.stdin.on('error',()=>{});child.stdin.end(prompt);
30
- });
31
- if(!fs.existsSync(output))throw new Error('Codex returned no result for '+phase+'. Retry the revision.');
22
+ args.push('-');
23
+ const attempt=async number=>{
24
+ const started=Date.now();const diagnostics={phase,attempt:number,model:model||'Codex default (unresolved)',reasoningEffort:effort,startedAt:new Date().toISOString(),events:0,lastEvent:null,lastItem:null,usage:null};
25
+ await new Promise((resolve,reject)=>{
26
+ const env=codexEnvironment();const executable=codexCommand();
27
+ const child=spawn(executable.command,[...executable.prefix,...args],{env,stdio:['pipe','pipe','pipe'],detached:process.platform!=='win32'});let stderr='',bytes=0,finished=false,pending='';
28
+ const heartbeat=setInterval(()=>atomic(path.join(dir,`phase-${phase}.json`),{...diagnostics,durationMs:Date.now()-started,running:true}),60000);heartbeat.unref();
29
+ const stop=()=>{try{if(process.platform!=='win32')process.kill(-child.pid,'SIGTERM');else child.kill('SIGTERM');}catch{}setTimeout(()=>{try{if(process.platform!=='win32')process.kill(-child.pid,'SIGKILL');else child.kill('SIGKILL');}catch{}},1500).unref();};
30
+ const abort=()=>{stop();done(failure('Preparation cancelled',false));};const timer=setTimeout(()=>{stop();done(failure(`${label} took longer than ${Math.round(deadlineMs/60000)} minutes.${diagnostics.lastProviderError?' Last provider error: '+diagnostics.lastProviderError:''}`,true));},deadlineMs);
31
+ function done(error){if(finished)return;finished=true;atomic(path.join(dir,`phase-${phase}.json`),{...diagnostics,durationMs:Date.now()-started,error:error?.message||null});clearTimeout(timer);clearInterval(heartbeat);signal?.removeEventListener('abort',abort);error?reject(error):resolve();}
32
+ signal?.addEventListener('abort',abort,{once:true});if(signal?.aborted){abort();return;}
33
+ child.stdout.on('data',chunk=>{pending+=chunk.toString();const lines=pending.split('\n');pending=lines.pop();for(const line of lines){try{const event=JSON.parse(line);diagnostics.events++;diagnostics.lastEvent=event.type;if(event.item?.type)diagnostics.lastItem=event.item.type;if(event.usage)diagnostics.usage=event.usage;if(event.type==='error'||event.type==='turn.failed'){diagnostics.lastProviderError=providerError(event.message||event.error?.message);onProgress(`${label} · Codex reported an error${event.type==='error'?'; waiting for retry':''}`);atomic(path.join(dir,`phase-${phase}.json`),{...diagnostics,durationMs:Date.now()-started,running:true});const limit=/max_output_tokens/.test(diagnostics.lastProviderError);if(event.type==='turn.failed'||limit){stop();done(failure('Codex could not complete '+label+': '+diagnostics.lastProviderError,!limit));}}}catch{}}bytes+=chunk.length;if(bytes>5_000_000){stop();done(failure('Interpretation output exceeded its size limit',false));}});
34
+ child.stderr.on('data',chunk=>{stderr=(stderr+chunk.toString()).slice(-6000);});child.on('error',e=>done(failure(e.code==='ENOENT'?'Codex CLI is not installed. Open Studio settings to reconnect.':e.message,false)));child.on('close',code=>done(code===0?null:/rate.limit|usage.limit|quota/i.test(stderr)?failure('Codex usage limit reached. Retry after your allowance resets.',false):/auth|login|unauthorized|401/i.test(stderr)?failure('Codex needs authentication. Open Studio settings to reconnect, then retry.',false):failure(`Codex could not complete this revision (exit ${code}). Check codex login status and retry.`,true)));
35
+ child.stdin.on('error',()=>{});child.stdin.end(prompt);
36
+ });
37
+ if(!fs.existsSync(output))throw failure('Codex returned no result for '+phase+'. Retry the revision.',true);
38
+ };
39
+ onProgress(label);
40
+ try{await attempt(1);}
41
+ catch(error){if(!error.retry||signal?.aborted)throw error;onProgress(`${label} · trying again`);await attempt(2);}
32
42
  return schema.parse(readJSON(output));
33
43
  }finally{fs.rmSync(temp,{recursive:true,force:true});}
34
44
  }
@@ -89,17 +89,19 @@ EVIDENCE:\n${JSON.stringify(evidence)}`);
89
89
  const critique=await call(`visual-review-${round}`,schema,`Act as a demanding design reviewer. First ${images.length} attached images are SOURCE captures in this order: ${JSON.stringify(capture.screenshots.filter(s=>images.includes(path.join(dir,s.file))).map(s=>s.id))}. Last four are GENERATED experiment 1 desktop/mobile, experiment 2 desktop/mobile. Compare rendered visual evidence, not the promise of the brief. Score identity transfer, task-specific composition, visual craft, interaction each 1–5. 3 means credible, 5 exceptional. Flag flattened artwork, diminished visual scale, wrong typography/hierarchy, generic repeated layouts, decorative assets disconnected from the task, clipping and failed interactions. Do not reward copying the source subject. Check if the two concepts actually differ. Ask for specific implementable fixes if any dimension is below 3 or functional checks fail. Aesthetic scores are advisory, not user approval. Direction: ${brief}\nBrowser checks: ${JSON.stringify(validation)}`, [...images,...previews]);
90
90
  atomic(path.join(dir,`visual-review-${round}.json`),critique);return critique;
91
91
  };
92
- onProgress('Comparing the rendered experiments with the reference');let critique=await review('initial');let repaired=false;
93
- for(let i=0;i<2;i++){
94
- const c=critique.experiments[i];const failures=validation.checks.filter(check=>check.demo===i+1&&!check.passed);
95
- if(!critique.distinct||c.needsRevision||Math.min(c.identity,c.composition,c.craft,c.interaction)<3||failures.length){
92
+ onProgress('Comparing the rendered experiments with the reference');let critique=await review('initial');
93
+ const needsWork=i=>{const c=critique.experiments[i];return !critique.distinct||c.needsRevision||Math.min(c.identity,c.composition,c.craft,c.interaction)<3||validation.checks.some(check=>check.demo===i+1&&!check.passed);};
94
+ const repairs=[0,1].filter(needsWork);const repaired=repairs.length>0;
95
+ if(repaired){
96
+ await runTogether(repairs.map(i=>async refineSignal=>{
97
+ const c=critique.experiments[i];const failures=validation.checks.filter(check=>check.demo===i+1&&!check.passed);
96
98
  onProgress(`Refining experiment ${i+1} after visual and interaction review`);
97
- const artwork=c.craft<3?await illustrate(i,`refine-artwork-${i+1}`,signal,c.issues.join(' ')):result.demos[i].artwork;
98
- const build=await buildInterface(i,artwork,signal,{review:c,comparison:critique.comparison,failures,html:result.demos[i].html,tests:result.demos[i].tests});
99
- atomic(path.join(dir,`refine-experiment-${i+1}.json`),build);result.demos[i]={...plan.concepts[i],...build,artwork};repaired=true;
100
- }
99
+ const artwork=c.craft<3?await illustrate(i,`refine-artwork-${i+1}`,refineSignal,c.issues.join(' ')):result.demos[i].artwork;
100
+ const build=await buildInterface(i,artwork,refineSignal,{review:c,comparison:critique.comparison,failures,html:result.demos[i].html,tests:result.demos[i].tests});
101
+ atomic(path.join(dir,`refine-experiment-${i+1}.json`),build);result.demos[i]={...plan.concepts[i],...build,artwork};
102
+ }),signal);
103
+ write();onProgress('Checking the refined experiments');validation=await render(dir,{signal});critique=await review('final');
101
104
  }
102
- if(repaired){write();onProgress('Checking the refined experiments');validation=await render(dir,{signal});critique=await review('final');}
103
105
  result.visualReview={...critique,repaired,scope:'Automated visual critique, advisory only. Your approval is still required.'};
104
106
  result.limitations=[...new Set([...plan.limitations,...result.demos.flatMap(d=>[...d.limitations,...d.artwork.limitations]),...critique.experiments.flatMap((c,i)=>c.issues.map(x=>`Experiment ${i+1} review: ${x}`))])];
105
107
  atomic(path.join(dir,'interpretation.json'),result);atomic(path.join(dir,'generation.json'),{provider:'Codex CLI',durationMs:Date.now()-started,createdAt:new Date().toISOString(),schemaVersion:2,model:model||'Codex default (unresolved)',reasoningEffort:{direction:'high',build:'medium',review:'medium'},repaired});return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antisketch",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=20.17"
@@ -33,6 +33,22 @@
33
33
  "design-vault": "bin/design-vault"
34
34
  },
35
35
  "description": "A local workshop for turning design references into reusable agent skills.",
36
+ "license": "MIT",
37
+ "author": "Harsh Joshi <harsh.joshi.pth@gmail.com>",
38
+ "homepage": "https://github.com/josharsh/antisketch#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/josharsh/antisketch/issues"
41
+ },
42
+ "keywords": [
43
+ "design",
44
+ "design-system",
45
+ "design-tokens",
46
+ "agent-skills",
47
+ "codex",
48
+ "claude-code",
49
+ "playwright",
50
+ "mcp"
51
+ ],
36
52
  "os": [
37
53
  "darwin"
38
54
  ],
@@ -56,7 +72,8 @@
56
72
  "app/public/brand/mark.svg",
57
73
  "app/public/brand/favicon.svg",
58
74
  "app/public/brand/identity.json",
59
- "app/public/brand/identity.css"
75
+ "app/public/brand/identity.css",
76
+ "LICENSE"
60
77
  ],
61
78
  "publishConfig": {
62
79
  "access": "public",