analyzthis_design 2.0.0 → 2.1.0

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.
Files changed (69) hide show
  1. package/HOW-TO-USE.md +436 -0
  2. package/README.md +29 -13
  3. package/agents/cards/evolve-check.md +38 -0
  4. package/agents/manifests/evolve-check.json +16 -0
  5. package/dist/HOW-TO-USE.md +15 -3
  6. package/dist/README.md +29 -13
  7. package/dist/agents/cards/evolve-check.md +38 -0
  8. package/dist/agents/manifests/evolve-check.json +16 -0
  9. package/dist/bin/cli.js +1225 -1
  10. package/dist/lib/cache.js +111 -1
  11. package/dist/lib/chunk-executor.js +219 -1
  12. package/dist/lib/chunk-models.js +228 -1
  13. package/dist/lib/chunk-planner.js +328 -1
  14. package/dist/lib/chunk-router.js +66 -1
  15. package/dist/lib/chunk-run.js +199 -1
  16. package/dist/lib/chunk-synthesis.js +176 -1
  17. package/dist/lib/chunk-telemetry.js +88 -1
  18. package/dist/lib/collect.js +858 -1
  19. package/dist/lib/cost.js +119 -1
  20. package/dist/lib/dedup.js +167 -1
  21. package/dist/lib/deliberation.js +721 -1
  22. package/dist/lib/design-spec.js +236 -1
  23. package/dist/lib/evolution-metrics.js +197 -0
  24. package/dist/lib/evolve.js +361 -1
  25. package/dist/lib/export.js +77 -1
  26. package/dist/lib/feedback-submit.js +324 -1
  27. package/dist/lib/feedback.js +182 -1
  28. package/dist/lib/host-llm.js +251 -1
  29. package/dist/lib/install.js +301 -1
  30. package/dist/lib/knowledge.js +384 -1
  31. package/dist/lib/lessons.js +217 -1
  32. package/dist/lib/moodboard.js +563 -1
  33. package/dist/lib/orchestrator/run.js +935 -1
  34. package/dist/lib/outcome.js +193 -1
  35. package/dist/lib/platforms.js +166 -1
  36. package/dist/lib/provider.js +57 -1
  37. package/dist/lib/query-expander.js +83 -1
  38. package/dist/lib/ranker.js +105 -1
  39. package/dist/lib/reference-pack.js +221 -0
  40. package/dist/lib/research.js +143 -1
  41. package/dist/lib/retrieve.js +131 -1
  42. package/dist/lib/session.js +185 -1
  43. package/dist/lib/source-discovery.js +486 -1
  44. package/dist/lib/synthesis.js +155 -1
  45. package/dist/lib/token-gate.js +46 -1
  46. package/dist/skills/design-reference/google-fonts.csv +1924 -1924
  47. package/dist/skills/design-reference/products.csv +162 -162
  48. package/dist/skills/design-reference/schema.json +159 -0
  49. package/dist/skills/design-reference/stacks/angular.csv +1 -1
  50. package/dist/skills/design-reference/stacks/astro.csv +1 -1
  51. package/dist/skills/design-reference/stacks/laravel.csv +2 -2
  52. package/dist/skills/design-reference/stacks/threejs.csv +54 -54
  53. package/dist/skills/design-reference/styles.csv +85 -85
  54. package/dist/skills/design-reference/typography.csv +75 -74
  55. package/dist/skills/design-reference/ui-reasoning.csv +1 -1
  56. package/dist/skills/evolve-check/SKILL.md +106 -0
  57. package/package.json +8 -8
  58. package/scripts/validate-csvs.js +197 -0
  59. package/skills/design-reference/google-fonts.csv +1924 -1924
  60. package/skills/design-reference/products.csv +162 -162
  61. package/skills/design-reference/schema.json +159 -0
  62. package/skills/design-reference/stacks/angular.csv +1 -1
  63. package/skills/design-reference/stacks/astro.csv +1 -1
  64. package/skills/design-reference/stacks/laravel.csv +2 -2
  65. package/skills/design-reference/stacks/threejs.csv +54 -54
  66. package/skills/design-reference/styles.csv +85 -85
  67. package/skills/design-reference/typography.csv +75 -74
  68. package/skills/design-reference/ui-reasoning.csv +1 -1
  69. package/skills/evolve-check/SKILL.md +106 -0
@@ -1,2 +1,859 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';const fs=require('fs'),path=require('path'),os=require('os'),crypto=require('crypto'),session=require('./session'),knowledge=require('./knowledge'),cache=require('./cache'),research=require('./research'),{discoverKnowledgeSources,writeSourceManifestNotes,connectDiscoveredSources}=require('./source-discovery'),{resolvePackageRoot}=require('./platforms'),PACKAGE_ROOT=resolvePackageRoot(__dirname),HARD_EXCLUDES=new Set(['node_modules','dist','build','.next','.nuxt','coverage','.git','.turbo','.vercel','.cache','out','.analyzthis_design','vendor','__pycache__','.venv','venv','target']),MAX_FILE_BYTES=0x30d40,MAX_EXTRACT_LINES=0x28,BATCH_SIZE=0x6,DEFAULT_WEB_LIMIT=0xa,WEB_SOURCE_FOLDERS=new Set(['PRDs','Product','Research','Design']),URL_RE=/https?:\/\/[^\s)'">\]`]+/gi;function loadCollectConfig(){const a=path['join'](os['homedir'](),'.analyzthis_design','config.json');if(!fs['existsSync'](a))return{};try{return JSON['parse'](fs['readFileSync'](a,'utf8'));}catch{return{};}}function normalizeUrl(a){return String(a)['replace'](/^[`'"]+|[`'"]+$/g,'')['replace'](/[.,;:!?)]+$/g,'');}function isResearchableUrl(a){if(!a||a['length']<0xc)return![];if(/[{}]/['test'](a))return![];try{const b=new URL(a);if(!['http:','https:']['includes'](b['protocol']))return![];const c=b['hostname']['toLowerCase']();if(!c['includes']('.'))return![];if(c==='localhost'||c['startsWith']('127.')||c==='0.0.0.0')return![];const d=b['pathname']['toLowerCase']();if(/\.(png|jpe?g|gif|svg|ico|webp|woff2?|ttf|eot|css|js|map|zip|tar|gz|pdf)$/i['test'](d))return![];if(c==='www.npmjs.com'||c==='npmjs.com')return![];if(c==='example.com'||c==='www.example.com')return![];if(c==='github.com'&&/\/(blob|raw|tree)\//['test'](b['pathname']))return![];return!![];}catch{return![];}}function extractUrlsFromText(a){const b=String(a)['match'](URL_RE)||[];return b['map'](normalizeUrl)['filter'](isResearchableUrl);}function discoverWebUrls({cwd:a,notes:b,config:c,webLimit:d}){const e=c['collect']||{},f=d??e['web_limit']??DEFAULT_WEB_LIMIT,g=new Set(),h=[],i=(k,l)=>{const m=normalizeUrl(k);if(!isResearchableUrl(m)||g['has'](m))return;g['add'](m),h['push']({'url':m,'source':l});};for(const k of[...e['web_urls']||[],...c['research']?.['urls']||[]]){i(k,'config');}if(e['web_from_repo']!==![]){const l=path['join'](a,'README.md');if(fs['existsSync'](l))try{for(const m of extractUrlsFromText(fs['readFileSync'](l,'utf8')))i(m,'README.md');}catch{}for(const n of b){if(!WEB_SOURCE_FOLDERS['has'](n['classifier']['folder']))continue;const o=path['join'](a,n['sourceRel']);try{for(const p of extractUrlsFromText(fs['readFileSync'](o,'utf8')))i(p,n['sourceRel']);}catch{}}}const j=e['web_queries']||c['research']?.['queries']||[];return{'urls':h['slice'](0x0,f),'queries':j['slice'](0x0,f),'limit':f,'totalDiscovered':h['length']};}async function collectWebResearch({urls:a,queries:b,projectId:c,dryRun:d}){const e={'fetched':[],'failed':[],'queries':[],'dryRun':!!d};if(d)return e['urls']=a,e;for(const {url:f,source:g}of a){try{console['log']('\x20\x20\x20•\x20'+f+'\x20\x20(from\x20'+g+')');const h=await research['researchUrl']({'url':f,'project':c});e['fetched']['push']({'url':f,'source':g,'chars':h['chars']});}catch(i){e['failed']['push']({'url':f,'source':g,'error':i['message']}),console['log']('\x20\x20\x20\x20\x20⚠\x20\x20'+i['message']);}}for(const j of b){try{console['log']('\x20\x20\x20•\x20query:\x20\x22'+j+'\x22');const k=await research['researchQuery']({'query':j,'project':c});e['queries']['push']({'query':j,'mode':k['mode'],'chars':k['chars']});}catch(l){e['failed']['push']({'query':j,'error':l['message']}),console['log']('\x20\x20\x20\x20\x20⚠\x20\x20'+l['message']);}}return e;}const CLASSIFIERS=[{'folder':'PRDs','category':'prd','tags':['prd'],'match':(a,b)=>/^(readme|contributing|changelog)\.md$/i['test'](b)||/\b(prd|requirements|user[-_]?stor|acceptance|epic|jtbd)\b/i['test'](a)||/^docs\//i['test'](a)&&/\.md$/i['test'](b)},{'folder':'Brand','category':'brand','tags':['brand','design'],'match':(a,b)=>/tailwind\.config\./i['test'](b)||/^(globals|global|tokens|theme|variables)\.(css|scss|ts|js)$/i['test'](b)||/design[-_]?system|brand|tokens?\./i['test'](a)||/\.css$/i['test'](b)&&/(theme|token|color|brand)/i['test'](a)},{'folder':'Product','category':'product','tags':['product'],'match':a=>/\b(roadmap|vision|product|okr|kpi|north[-_]?star)\b/i['test'](a)&&/\.md$/i['test'](a)},{'folder':'Research','category':'research','tags':['research'],'match':a=>/\b(research|interview|survey|insight|persona|usability)\b/i['test'](a)&&/\.md$/i['test'](a)},{'folder':'Pages','category':'design','tags':['design','page'],'match':(a,b)=>/(^|\/)(app|pages|src\/pages)\//i['test'](a)&&(/page\.(tsx|jsx|ts|js|vue|svelte)$/i['test'](b)||/^index\.(tsx|jsx|ts|js)$/i['test'](b))},{'folder':'Components','category':'design','tags':['design','component'],'match':(a,b)=>/(^|\/)(components|src\/ui|src\/components|ui)\//i['test'](a)&&/\.(tsx|jsx|vue|svelte)$/i['test'](b)&&!/\.(test|spec|stories)\./i['test'](b)},{'folder':'Design','category':'design','tags':['design'],'match':(a,b)=>/\.md$/i['test'](b)&&/\b(design|figma|wireframe|layout|ux|ui)\b/i['test'](a)},{'folder':'Tech','category':'tech','tags':['tech'],'match':(a,b)=>b==='package.json'||/^(tsconfig|next\.config|vite\.config|astro\.config|nuxt\.config)/i['test'](b)||/(^|\/)(api|app\/api|pages\/api|server)\//i['test'](a)&&/\.(ts|js)$/i['test'](b)}];function loadGitignore(a){const b=[],c=path['join'](a,'.gitignore');if(!fs['existsSync'](c))return b;for(const d of fs['readFileSync'](c,'utf8')['split']('\x0a')){const e=d['trim']();if(!e||e['startsWith']('#')||e['startsWith']('!'))continue;b['push'](e['replace'](/\/$/,''));}return b;}function isIgnored(a,b){const c=a['split'](path['sep']);if(c['some'](d=>HARD_EXCLUDES['has'](d)||d['startsWith']('.'))){if(c['some'](d=>HARD_EXCLUDES['has'](d)))return!![];if(c['some'](d=>d['startsWith']('.')&&d!=='.'))return!![];}for(const d of b){const e=d['replace'](/^\*\*\//,'')['replace'](/\*\*/g,'');if(!e)continue;if(a===d||a['startsWith'](d+path['sep'])||a['includes'](path['sep']+d+path['sep']))return!![];if(d['endsWith']('*')&&a['startsWith'](d['slice'](0x0,-0x1)))return!![];if(d['startsWith']('*.')&&a['endsWith'](d['slice'](0x1)))return!![];}return![];}function walkFiles(a,b,c=[]){const d=e=>{let f;try{f=fs['readdirSync'](e,{'withFileTypes':!![]});}catch{return;}for(const g of f){const h=path['join'](e,g['name']),i=path['relative'](a,h);if(isIgnored(i,b))continue;if(g['isDirectory']())d(h);else{if(g['isFile']())c['push']({'full':h,'rel':i,'base':g['name']});}}};return d(a),c;}function sha1(a){return crypto['createHash']('sha1')['update'](a)['digest']('hex')['slice'](0x0,0xc);}function slugify(a){return String(a)['replace'](/\.[^.]+$/,'')['replace'](/[^a-zA-Z0-9-_]+/g,'-')['replace'](/^-+|-+$/g,'')['slice'](0x0,0x50)||'note';}function extractSnippet(a){let b='';try{const g=fs['statSync'](a);g['size']>MAX_FILE_BYTES?b=fs['readFileSync'](a,'utf8')['slice'](0x0,MAX_FILE_BYTES):b=fs['readFileSync'](a,'utf8');}catch{return{'lines':[],'imports':[],'exports':[],'figma':[]};}const c=b['split']('\x0a')['slice'](0x0,MAX_EXTRACT_LINES),d=[],e=[],f=[];for(const h of b['split']('\x0a')['slice'](0x0,0xc8)){const i=h['match'](/from\s+['"]([^'"]+)['"]/);if(i)d['push'](i[0x1]);(/^export\s+(default\s+)?(function|const|class|async)/['test'](h['trim']())||/^export\s*\{/['test'](h['trim']()))&&e['push'](h['trim']()['slice'](0x0,0x78));const j=h['match'](/https?:\/\/(?:www\.)?figma\.com\/[^\s)'"]+/gi);if(j)f['push'](...j);}return{'lines':c,'imports':[...new Set(d)]['slice'](0x0,0x14),'exports':e['slice'](0x0,0xf),'figma':[...new Set(f)]['slice'](0x0,0x5)};}function classifyFile(a,b){for(const d of CLASSIFIERS){if(d['match'](a,b))return d;}return null;}function frontmatter(a){const b=['---'];for(const [c,d]of Object['entries'](a)){if(Array['isArray'](d))b['push'](c+':\x20['+d['map'](e=>JSON['stringify'](e))['join'](',\x20')+']');else{if(typeof d==='boolean')b['push'](c+':\x20'+d);else b['push'](c+':\x20'+JSON['stringify'](String(d)));}}return b['push']('---'),b['join']('\x0a');}function buildDraftNote({title:a,rel:b,classifier:c,extract:d,related:e}){const f=new Date()['toISOString']()['split']('T')[0x0],g=sha1(d['lines']['join']('\x0a')),h=frontmatter({'title':a,'tags':c['tags'],'source':b,'category':c['category'],'collected_at':f,'content_hash':g,'enriched':![]}),i=e['length']?e['map'](k=>'-\x20[['+k+']]')['join']('\x0a'):'-\x20_(none\x20yet)_',j=['##\x20Purpose','Draft\x20extract\x20from\x20`'+b+'`.\x20Awaiting\x20Kavi\x20enrichment.','','##\x20Raw\x20extract','```',d['lines']['join']('\x0a')['slice'](0x0,0xbb8),'```','','##\x20Exports',d['exports']['length']?d['exports']['map'](k=>'-\x20`'+k+'`')['join']('\x0a'):'-\x20_(none\x20detected)_','','##\x20Imports',d['imports']['length']?d['imports']['map'](k=>'-\x20`'+k+'`')['join']('\x0a'):'-\x20_(none\x20detected)_','',...d['figma']['length']?['##\x20Figma\x20links',...d['figma']['map'](k=>'-\x20'+k),'']:[],'##\x20Related',i,'']['join']('\x0a');return{'fm':h,'body':j,'contentHash':g};}function scanAndDraft({cwd:a,vaultPath:b,dryRun:d}){const e=loadGitignore(a),g=walkFiles(a,e),h=[],i=[];for(const l of g){const o=classifyFile(l['rel'],l['base']);if(!o)continue;const p=extractSnippet(l['full']),q=slugify(l['base']);i['push']({...l,'classifier':o,'extract':p,'title':q});}const j=i['filter'](r=>r['classifier']['folder']==='Pages')['map'](r=>'Pages/'+r['title']),k=i['filter'](r=>r['classifier']['folder']==='Components')['map'](r=>'Components/'+r['title']);for(const r of i){let s=[];if(r['classifier']['folder']==='Components')s=j['filter'](y=>{const z=y['split']('/')[0x1]['toLowerCase']();return r['extract']['imports']['some'](A=>A['toLowerCase']()['includes'](r['title']['toLowerCase']()))||z['includes'](r['title']['toLowerCase']()['slice'](0x0,0x6));})['slice'](0x0,0x5);else r['classifier']['folder']==='Pages'&&(s=k['filter'](y=>{const z=y['split']('/')[0x1]['toLowerCase']();return r['extract']['imports']['some'](A=>A['toLowerCase']()['includes'](z));})['slice'](0x0,0x8));const {fm:t,body:u,contentHash:v}=buildDraftNote({'title':r['title'],'rel':r['rel'],'classifier':r['classifier'],'extract':r['extract'],'related':s});let w=path['join'](r['classifier']['folder'],r['title']+'.md'),x=0x2;while(h['some'](y=>y['vaultRel']===w)){w=path['join'](r['classifier']['folder'],r['title']+'-'+x+'.md'),x+=0x1;}h['push']({'vaultRel':w,'fullOut':path['join'](b,w),'title':r['title'],'classifier':r['classifier'],'contentHash':v,'draft':t+'\x0a\x0a'+u,'sourceRel':r['rel']});}if(!d){fs['mkdirSync'](b,{'recursive':!![]});for(const y of h){if(fs['existsSync'](y['fullOut']))try{const z=fs['readFileSync'](y['fullOut'],'utf8'),A=z['match'](/content_hash:\s*"?([a-f0-9]+)"?/),B=/enriched:\s*true/['test'](z);if(A&&A[0x1]===y['contentHash']&&B){y['skipped']=!![],y['draft']=z;continue;}}catch{}fs['mkdirSync'](path['dirname'](y['fullOut']),{'recursive':!![]}),fs['writeFileSync'](y['fullOut'],y['draft']);}writeMeta(b,h,a);}return{'filesScanned':g['length'],'notesWritten':h['filter'](C=>!C['skipped'])['length'],'notesSkipped':h['filter'](C=>C['skipped'])['length'],'notes':h};}function writeMeta(a,b,c){const d={};for(const f of b){const g=f['classifier']['folder'];if(!d[g])d[g]=[];d[g]['push'](f);}const e=['---','title:\x20\x22Knowledge\x20Vault\x20Index\x22','tags:\x20[meta,\x20moc]','collected_at:\x20\x22'+new Date()['toISOString']()['split']('T')[0x0]+'\x22','---','','#\x20Knowledge\x20Vault\x20Index','','Source\x20repo:\x20`'+c+'`','','##\x20Map\x20of\x20content',''];for(const [h,i]of Object['entries'](d)['sort']()){e['push']('###\x20'+h,'');for(const j of i){const k=j['vaultRel']['replace'](/\.md$/,'')['replace'](/\\/g,'/');e['push']('-\x20[['+k+'|'+j['title']+']]\x20—\x20`'+j['sourceRel']+'`');}e['push']('');}fs['mkdirSync'](path['join'](a,'_meta'),{'recursive':!![]}),fs['writeFileSync'](path['join'](a,'_meta','index.md'),e['join']('\x0a')),fs['writeFileSync'](path['join'](a,'_meta','last-sync.md'),['---','title:\x20\x22Last\x20sync\x22','---','','#\x20Last\x20sync','','-\x20**When:**\x20'+new Date()['toISOString'](),'-\x20**Repo:**\x20'+c,'-\x20**Notes:**\x20'+b['length'],'']['join']('\x0a'));}function loadKaviCard(){const a=path['join'](PACKAGE_ROOT,'agents','cards','kavi.md');if(fs['existsSync'](a))return fs['readFileSync'](a,'utf8');return'You\x20are\x20Kavi,\x20the\x20Knowledge\x20Archivist.\x20Rewrite\x20draft\x20extracts\x20into\x20concise\x20Obsidian\x20notes.\x20Do\x20not\x20invent\x20files\x20or\x20APIs.';}function hasAnyApiKey(){return!!(process.env.ANTHROPIC_API_KEY||process.env.OPENAI_API_KEY||process.env.GEMINI_API_KEY||process.env.GOOGLE_API_KEY||process.env.ZAI_API_KEY||process.env.ZHIPU_API_KEY);}function pickEnrichProvider(a){if(process.env.OPENAI_API_KEY)return{'provider':'openai','model':a['orchestrator']?.['tiers']?.['structured']?.['model']||'gpt-4o-mini'};if(process.env.GEMINI_API_KEY||process.env.GOOGLE_API_KEY)return{'provider':'google','model':'gemini-2.5-flash'};if(process.env.ZAI_API_KEY||process.env.ZHIPU_API_KEY)return{'provider':'zai','model':'glm-4.5-flash'};if(process.env.ANTHROPIC_API_KEY)return{'provider':'anthropic','model':'claude-haiku-4-5-20251001'};return null;}function parseEnrichedBatch(a,b){const c=a['split'](/---NOTE\s+\d+---/i)['map'](d=>d['trim']())['filter'](Boolean);if(c['length']>=b)return c['slice'](0x0,b);if(c['length']===0x1&&b===0x1)return c;while(c['length']<b)c['push'](null);return c['slice'](0x0,b);}async function enrichNotes({notes:a,limit:b,provider:c,model:d}){const {callLlm:e,loadConfig:f}=require('./orchestrator/run'),g=f(),h=c&&d?{'provider':c,'model':d}:pickEnrichProvider(g);if(!h)return{'enriched':0x0,'skipped':a['length'],'reason':'No\x20API\x20key\x20set\x20—\x20wrote\x20draft-only\x20vault.','llm_calls':0x0,'input_tokens_est':0x0,'output_tokens_est':0x0};const k=a['filter'](q=>!q['skipped'])['slice'](0x0,b==null?a['length']:b),l=[loadKaviCard(),'','For\x20each\x20draft\x20note\x20below,\x20rewrite\x20ONLY\x20the\x20body\x20(keep\x20the\x20YAML\x20frontmatter\x20I\x20give\x20you).','Output\x20format:\x20for\x20each\x20note,\x20emit\x20exactly:','---NOTE\x201---','<full\x20markdown\x20including\x20the\x20original\x20frontmatter\x20with\x20enriched:\x20true>','---NOTE\x202---','...','Rules:\x20purpose\x20+\x20key\x20facts\x20+\x20Related\x20wikilinks.\x20Do\x20NOT\x20invent\x20files,\x20APIs,\x20or\x20components\x20not\x20in\x20the\x20Raw\x20extract.']['join']('\x0a');let m=0x0,n=0x0,o=0x0,p=0x0;for(let q=0x0;q<k['length'];q+=BATCH_SIZE){const r=k['slice'](q,q+BATCH_SIZE),s=r['map']((v,w)=>'---NOTE\x20'+(w+0x1)+'---\x0a'+v['draft'])['join']('\x0a\x0a'),t=await e({'provider':h['provider'],'model':h['model'],'system':l,'user':s,'maxTokens':0xfa0});n+=0x1,o+=Math['ceil']((l['length']+s['length'])/0x4),p+=Math['ceil'](t['length']/0x4);const u=parseEnrichedBatch(t,r['length']);for(let v=0x0;v<r['length'];v++){let w=u[v];if(!w)continue;!/enriched:\s*true/['test'](w)&&(w=w['replace'](/enriched:\s*false/,'enriched:\x20true'),!/enriched:/['test'](w)&&w['startsWith']('---')&&(w=w['replace'](/^---\n/,'---\x0aenriched:\x20true\x0a'))),!/content_hash:/['test'](w)&&r[v]['contentHash']&&(w=w['replace'](/^---\n/,'---\x0acontent_hash:\x20\x22'+r[v]['contentHash']+'\x22\x0a')),fs['writeFileSync'](r[v]['fullOut'],w),r[v]['draft']=w,m+=0x1;}}return{'enriched':m,'skipped':a['length']-m,'reason':null,'llm_calls':n,'input_tokens_est':o,'output_tokens_est':p,'provider':h['provider'],'model':h['model']};}async function collect(a={}){const b=path['resolve'](a['cwd']||process['cwd']()),c=a['project']||session['getProjectId'](b),e=path['resolve'](a['vault']||path['join'](os['homedir'](),'.analyzthis_design','vaults',c)),f=a['enrich']!==![],g=!!a['dryRun'],h=a['limit']!=null?a['limit']:null,i=a['web']!==![],j=a['discoverSources']!==![],k=loadCollectConfig(),{resolveTargets:l}=require('./platforms'),m=l(a['target']||'cursor')||['cursor'];!session['show']({'project':c})&&session['init']({'project':c});const o=[];if(g)o['push']('dry-run');else{if(j)o['push']('discover');if(i)o['push']('web');if(f)o['push']('enrich');o['push']('sync');}console['log']('\x0a📚\x20Kavi\x20—\x20Collect\x20Knowledge'),console['log']('\x20\x20\x20Repo:\x20\x20'+b),console['log']('\x20\x20\x20Vault:\x20'+e),console['log']('\x20\x20\x20Mode:\x20\x20'+(o['join']('\x20+\x20')||'scan')+'\x0a');const q=scanAndDraft({'cwd':b,'vaultPath':e,'dryRun':g}),r=j?discoverKnowledgeSources({'cwd':b,'config':k}):[];console['log']('\x20\x20\x20Scanned\x20'+q['filesScanned']+'\x20file(s)\x20→\x20'+q['notes']['length']+'\x20note(s)\x20('+q['notesWritten']+'\x20written,\x20'+q['notesSkipped']+'\x20unchanged)');if(j&&r['length']>0x0){console['log']('\x0a🔍\x20Knowledge\x20sources:\x20'+r['length']+'\x20discovered\x20(Obsidian\x20vaults,\x20wikis,\x20referenced\x20paths)');for(const B of r['slice'](0x0,0x6)){console['log']('\x20\x20\x20•\x20['+B['confidence']+']\x20'+B['type']+':\x20'+B['path']);}if(r['length']>0x6)console['log']('\x20\x20\x20…\x20and\x20'+(r['length']-0x6)+'\x20more');}else j&&!g&&console['log']('\x0a🔍\x20Knowledge\x20sources:\x20none\x20discovered\x20(add\x20collect.source_paths\x20in\x20config\x20or\x20link\x20vaults\x20in\x20README)');const s=i?discoverWebUrls({'cwd':b,'notes':q['notes'],'config':k,'webLimit':a['webLimit']}):{'urls':[],'queries':[],'limit':0x0,'totalDiscovered':0x0};if(i&&(s['urls']['length']>0x0||s['queries']['length']>0x0))console['log']('\x0a🌐\x20Web\x20research:\x20'+s['urls']['length']+'\x20URL(s)'+(s['queries']['length']?',\x20'+s['queries']['length']+'\x20query\x20stub(s)':'')+(s['totalDiscovered']>s['urls']['length']?'\x20('+s['totalDiscovered']+'\x20found,\x20capped\x20at\x20'+s['limit']+')':''));else i&&!g&&console['log']('\x0a🌐\x20Web\x20research:\x20no\x20URLs\x20found\x20(add\x20collect.web_urls\x20in\x20~/.analyzthis_design/config.json\x20or\x20links\x20in\x20PRDs/README)');if(g){const C={};for(const D of q['notes']){C[D['classifier']['folder']]=(C[D['classifier']['folder']]||0x0)+0x1;}return console['log']('\x0a──\x20Dry\x20run\x20preview\x20──'),console['log'](JSON['stringify']({'vaultPath':e,'projectId':c,'byFolder':C,'knowledgeSources':r['map'](E=>({'path':E['path'],'type':E['type'],'confidence':E['confidence'],'discovered_from':E['discovered_from'],'markdown_files':E['stats']['markdown_files']})),'webUrls':s['urls']['map'](E=>({'url':E['url'],'source':E['source']})),'webQueries':s['queries'],'sample':q['notes']['slice'](0x0,0x8)['map'](E=>E['vaultRel'])},null,0x2)),console['log'](''),{'vaultPath':e,'projectId':c,'dryRun':!![],...q,'sources':r,'web':s,'enrich':null,'sync':null};}let u={'written':[]};j&&r['length']>0x0&&(console['log']('\x0a⏳\x20Writing\x20source\x20manifest\x20notes\x20(Sources/*.md)...'),u=writeSourceManifestNotes({'discoveries':r,'vaultPath':e,'dryRun':![]}),console['log']('\x20\x20\x20Wrote\x20'+u['written']['length']+'\x20manifest\x20note(s)\x20+\x20_meta/knowledge-sources.md'));let v={'fetched':[],'failed':[],'queries':[]};i&&(s['urls']['length']>0x0||s['queries']['length']>0x0)&&(console['log']('\x0a⏳\x20Fetching\x20web\x20research\x20into\x20web-context.md...'),v=await collectWebResearch({'urls':s['urls'],'queries':s['queries'],'projectId':c,'dryRun':![]}),console['log']('\x20\x20\x20Fetched\x20'+v['fetched']['length']+'\x20URL(s)'+(v['failed']['length']?',\x20'+v['failed']['length']+'\x20failed':'')));let w={'enriched':0x0,'skipped':q['notes']['length'],'reason':'enrichment\x20disabled','llm_calls':0x0,'input_tokens_est':0x0,'output_tokens_est':0x0};f&&(!hasAnyApiKey()?(console['log']('\x0a\x20\x20⚠\x20\x20No\x20LLM\x20API\x20key\x20found\x20—\x20writing\x20draft-only\x20vault.\x20Set\x20OPENAI_API_KEY\x20/\x20GEMINI_API_KEY\x20/\x20ANTHROPIC_API_KEY\x20/\x20ZAI_API_KEY\x20to\x20enrich.'),w['reason']='No\x20API\x20key\x20set\x20—\x20wrote\x20draft-only\x20vault.'):(console['log']('\x0a⏳\x20Enriching\x20notes\x20with\x20Kavi\x20(LLM)...'),w=await enrichNotes({'notes':q['notes'],'limit':h,'provider':a['provider'],'model':a['model']}),console['log']('\x20\x20\x20Enriched\x20'+w['enriched']+'\x20note(s)'+(w['reason']?'\x20('+w['reason']+')':''))));console['log']('\x0a⏳\x20Wiring\x20knowledge\x20bank...');const x=j?connectDiscoveredSources({'discoveries':r,'kaviVaultPath':e,'config':k,'autoConnect':k['collect']?.['auto_connect_discovered']!==![]}):{'connected':[],'skipped':[]};if(x['connected']['length']>0x0){console['log']('\x20\x20\x20Auto-connected\x20'+x['connected']['length']+'\x20source(s):');for(const E of x['connected'])console['log']('\x20\x20\x20•\x20'+E);}knowledge['connect']({'vaultPath':e,'tags':[],'include':[]});const y=knowledge['sync']({'targets':m});cache['invalidatePrefix']('kb:');if(y['message'])console['log']('\x20\x20⚠\x20\x20'+y['message']);else{console['log']('✅\x20Synced\x20'+y['synced']+'\x20note(s)\x20into\x20the\x20knowledge\x20bank.');for(const F of y['copiedTo']||[])console['log']('\x20\x20\x20•\x20'+F);}const z=session['show']({'project':c})||{},A=Array['isArray'](z['content_sources'])?z['content_sources']:[];if(!A['includes']('codebase_collect'))A['push']('codebase_collect');return session['update']({'project':c,'patch':{'content_sources':A,'vault_path':e,'last_collect_at':new Date()['toISOString'](),'digest':{...z['digest']||{},'task_map_summary':'Collected\x20knowledge\x20vault\x20('+q['notes']['length']+'\x20notes)\x20from\x20'+b,'experts':['kavi']},'metrics':{...z['metrics']||{},'llm_calls':w['llm_calls']||0x0,'experts_run':['kavi'],'input_tokens_est':w['input_tokens_est']||0x0,'output_tokens_est':w['output_tokens_est']||0x0,'mode':'collect','effort_log':w['model']?[{'persona':'kavi','effort':'standard','model':w['model'],'scoped_mode':null}]:[],'cost_usd':z['metrics']?.['cost_usd']||0x0}}}),console['log']('\x0a✅\x20Collect\x20complete.'),console['log']('\x20\x20\x20Vault:\x20'+e),console['log']('\x20\x20\x20Open\x20in\x20Obsidian,\x20or\x20run\x20/persona-orchestrator\x20—\x20personas\x20now\x20read\x20this\x20knowledge\x20bank.\x0a'),{'vaultPath':e,'projectId':c,'dryRun':![],'filesScanned':q['filesScanned'],'notes':q['notes']['length'],'notesWritten':q['notesWritten'],'notesSkipped':q['notesSkipped'],'web':v,'sources':{'discovered':r,'manifest':u,'connected':x['connected'],'skipped':x['skipped']},'enrich':w,'sync':y};}module['exports']={'collect':collect,'scanAndDraft':scanAndDraft,'classifyFile':classifyFile,'walkFiles':walkFiles,'discoverWebUrls':discoverWebUrls,'collectWebResearch':collectWebResearch,'extractUrlsFromText':extractUrlsFromText,'isResearchableUrl':isResearchableUrl,'HARD_EXCLUDES':HARD_EXCLUDES};
2
+ 'use strict';
3
+
4
+ /**
5
+ * Kavi — Collect Knowledge (v1.11)
6
+ *
7
+ * Scans the current codebase, writes an Obsidian-compatible vault, optionally
8
+ * LLM-enriches notes, then auto-connects + syncs into the knowledge bank so
9
+ * all critique personas read company context first.
10
+ *
11
+ * Usage (via CLI):
12
+ * npx analyzthis_design collect [--vault path] [--dry-run] [--no-enrich] [--limit N] [--target cursor|all]
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+ const crypto = require('crypto');
19
+ const session = require('./session');
20
+ const knowledge = require('./knowledge');
21
+ const cache = require('./cache');
22
+ const research = require('./research');
23
+ const {
24
+ discoverKnowledgeSources,
25
+ writeSourceManifestNotes,
26
+ connectDiscoveredSources,
27
+ } = require('./source-discovery');
28
+
29
+ const { resolvePackageRoot } = require('./platforms');
30
+ const PACKAGE_ROOT = resolvePackageRoot(__dirname);
31
+
32
+ const HARD_EXCLUDES = new Set([
33
+ 'node_modules', 'dist', 'build', '.next', '.nuxt', 'coverage', '.git',
34
+ '.turbo', '.vercel', '.cache', 'out', '.analyzthis_design', 'vendor',
35
+ '__pycache__', '.venv', 'venv', 'target',
36
+ ]);
37
+
38
+ const MAX_FILE_BYTES = 200_000;
39
+ const MAX_EXTRACT_LINES = 40;
40
+ const BATCH_SIZE = 6;
41
+ const DEFAULT_WEB_LIMIT = 10;
42
+ const WEB_SOURCE_FOLDERS = new Set(['PRDs', 'Product', 'Research', 'Design']);
43
+ const URL_RE = /https?:\/\/[^\s)'">\]`]+/gi;
44
+
45
+ function loadCollectConfig() {
46
+ const configPath = path.join(os.homedir(), '.analyzthis_design', 'config.json');
47
+ if (!fs.existsSync(configPath)) return {};
48
+ try { return JSON.parse(fs.readFileSync(configPath, 'utf8')); }
49
+ catch { return {}; }
50
+ }
51
+
52
+ function normalizeUrl(url) {
53
+ return String(url)
54
+ .replace(/^[`'"]+|[`'"]+$/g, '')
55
+ .replace(/[.,;:!?)]+$/g, '');
56
+ }
57
+
58
+ function isResearchableUrl(url) {
59
+ if (!url || url.length < 12) return false;
60
+ if (/[{}]/.test(url)) return false;
61
+ try {
62
+ const u = new URL(url);
63
+ if (!['http:', 'https:'].includes(u.protocol)) return false;
64
+ const host = u.hostname.toLowerCase();
65
+ if (!host.includes('.')) return false;
66
+ if (host === 'localhost' || host.startsWith('127.') || host === '0.0.0.0') return false;
67
+ const pathname = u.pathname.toLowerCase();
68
+ if (/\.(png|jpe?g|gif|svg|ico|webp|woff2?|ttf|eot|css|js|map|zip|tar|gz|pdf)$/i.test(pathname)) return false;
69
+ if (host === 'www.npmjs.com' || host === 'npmjs.com') return false;
70
+ if (host === 'example.com' || host === 'www.example.com') return false;
71
+ if (host === 'github.com' && /\/(blob|raw|tree)\//.test(u.pathname)) return false;
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ function extractUrlsFromText(text) {
79
+ const found = String(text).match(URL_RE) || [];
80
+ return found.map(normalizeUrl).filter(isResearchableUrl);
81
+ }
82
+
83
+ /**
84
+ * Discover external URLs from config + markdown in PRDs/Product/Research/Design/README.
85
+ */
86
+ function discoverWebUrls({ cwd, notes, config, webLimit }) {
87
+ const collectCfg = config.collect || {};
88
+ const limit = webLimit ?? collectCfg.web_limit ?? DEFAULT_WEB_LIMIT;
89
+ const seen = new Set();
90
+ const urls = [];
91
+
92
+ const add = (url, source) => {
93
+ const u = normalizeUrl(url);
94
+ if (!isResearchableUrl(u) || seen.has(u)) return;
95
+ seen.add(u);
96
+ urls.push({ url: u, source });
97
+ };
98
+
99
+ for (const u of [...(collectCfg.web_urls || []), ...(config.research?.urls || [])]) {
100
+ add(u, 'config');
101
+ }
102
+
103
+ if (collectCfg.web_from_repo !== false) {
104
+ const readme = path.join(cwd, 'README.md');
105
+ if (fs.existsSync(readme)) {
106
+ try {
107
+ for (const u of extractUrlsFromText(fs.readFileSync(readme, 'utf8'))) add(u, 'README.md');
108
+ } catch { /* ignore */ }
109
+ }
110
+ for (const note of notes) {
111
+ if (!WEB_SOURCE_FOLDERS.has(note.classifier.folder)) continue;
112
+ const full = path.join(cwd, note.sourceRel);
113
+ try {
114
+ for (const u of extractUrlsFromText(fs.readFileSync(full, 'utf8'))) add(u, note.sourceRel);
115
+ } catch { /* ignore */ }
116
+ }
117
+ }
118
+
119
+ const queries = collectCfg.web_queries || config.research?.queries || [];
120
+ return { urls: urls.slice(0, limit), queries: queries.slice(0, limit), limit, totalDiscovered: urls.length };
121
+ }
122
+
123
+ /**
124
+ * Fetch discovered URLs (and optional query stubs) into session web-context.md.
125
+ */
126
+ async function collectWebResearch({ urls, queries, projectId, dryRun }) {
127
+ const results = { fetched: [], failed: [], queries: [], dryRun: !!dryRun };
128
+ if (dryRun) {
129
+ results.urls = urls;
130
+ return results;
131
+ }
132
+
133
+ for (const { url, source } of urls) {
134
+ try {
135
+ console.log(` • ${url} (from ${source})`);
136
+ const r = await research.researchUrl({ url, project: projectId });
137
+ results.fetched.push({ url, source, chars: r.chars });
138
+ } catch (err) {
139
+ results.failed.push({ url, source, error: err.message });
140
+ console.log(` ⚠ ${err.message}`);
141
+ }
142
+ }
143
+
144
+ for (const query of queries) {
145
+ try {
146
+ console.log(` • query: "${query}"`);
147
+ const r = await research.researchQuery({ query, project: projectId });
148
+ results.queries.push({ query, mode: r.mode, chars: r.chars });
149
+ } catch (err) {
150
+ results.failed.push({ query, error: err.message });
151
+ console.log(` ⚠ ${err.message}`);
152
+ }
153
+ }
154
+
155
+ return results;
156
+ }
157
+
158
+ // ─── Classification rules (source → vault folder + category) ─────────────────
159
+
160
+ const CLASSIFIERS = [
161
+ {
162
+ folder: 'PRDs',
163
+ category: 'prd',
164
+ tags: ['prd'],
165
+ match: (rel, base) =>
166
+ /^(readme|contributing|changelog)\.md$/i.test(base) ||
167
+ /\b(prd|requirements|user[-_]?stor|acceptance|epic|jtbd)\b/i.test(rel) ||
168
+ /^docs\//i.test(rel) && /\.md$/i.test(base),
169
+ },
170
+ {
171
+ folder: 'Brand',
172
+ category: 'brand',
173
+ tags: ['brand', 'design'],
174
+ match: (rel, base) =>
175
+ /tailwind\.config\./i.test(base) ||
176
+ /^(globals|global|tokens|theme|variables)\.(css|scss|ts|js)$/i.test(base) ||
177
+ /design[-_]?system|brand|tokens?\./i.test(rel) ||
178
+ /\.css$/i.test(base) && /(theme|token|color|brand)/i.test(rel),
179
+ },
180
+ {
181
+ folder: 'Product',
182
+ category: 'product',
183
+ tags: ['product'],
184
+ match: (rel) => /\b(roadmap|vision|product|okr|kpi|north[-_]?star)\b/i.test(rel) && /\.md$/i.test(rel),
185
+ },
186
+ {
187
+ folder: 'Research',
188
+ category: 'research',
189
+ tags: ['research'],
190
+ match: (rel) => /\b(research|interview|survey|insight|persona|usability)\b/i.test(rel) && /\.md$/i.test(rel),
191
+ },
192
+ {
193
+ folder: 'Pages',
194
+ category: 'design',
195
+ tags: ['design', 'page'],
196
+ match: (rel, base) =>
197
+ /(^|\/)(app|pages|src\/pages)\//i.test(rel) &&
198
+ (/page\.(tsx|jsx|ts|js|vue|svelte)$/i.test(base) || /^index\.(tsx|jsx|ts|js)$/i.test(base)),
199
+ },
200
+ {
201
+ folder: 'Components',
202
+ category: 'design',
203
+ tags: ['design', 'component'],
204
+ match: (rel, base) =>
205
+ /(^|\/)(components|src\/ui|src\/components|ui)\//i.test(rel) &&
206
+ /\.(tsx|jsx|vue|svelte)$/i.test(base) &&
207
+ !/\.(test|spec|stories)\./i.test(base),
208
+ },
209
+ {
210
+ folder: 'Design',
211
+ category: 'design',
212
+ tags: ['design'],
213
+ match: (rel, base) =>
214
+ /\.md$/i.test(base) && /\b(design|figma|wireframe|layout|ux|ui)\b/i.test(rel),
215
+ },
216
+ {
217
+ folder: 'Tech',
218
+ category: 'tech',
219
+ tags: ['tech'],
220
+ match: (rel, base) =>
221
+ base === 'package.json' ||
222
+ /^(tsconfig|next\.config|vite\.config|astro\.config|nuxt\.config)/i.test(base) ||
223
+ /(^|\/)(api|app\/api|pages\/api|server)\//i.test(rel) && /\.(ts|js)$/i.test(base),
224
+ },
225
+ ];
226
+
227
+ // ─── Ignore helpers ──────────────────────────────────────────────────────────
228
+
229
+ function loadGitignore(cwd) {
230
+ const patterns = [];
231
+ const gi = path.join(cwd, '.gitignore');
232
+ if (!fs.existsSync(gi)) return patterns;
233
+ for (const line of fs.readFileSync(gi, 'utf8').split('\n')) {
234
+ const t = line.trim();
235
+ if (!t || t.startsWith('#') || t.startsWith('!')) continue;
236
+ patterns.push(t.replace(/\/$/, ''));
237
+ }
238
+ return patterns;
239
+ }
240
+
241
+ function isIgnored(rel, gitPatterns) {
242
+ const parts = rel.split(path.sep);
243
+ if (parts.some((p) => HARD_EXCLUDES.has(p) || p.startsWith('.'))) {
244
+ // Allow .md under docs that aren't hidden dirs; still block .git etc.
245
+ if (parts.some((p) => HARD_EXCLUDES.has(p))) return true;
246
+ // Dotfiles at root of scan (e.g. .env) — skip
247
+ if (parts.some((p) => p.startsWith('.') && p !== '.')) return true;
248
+ }
249
+ for (const pat of gitPatterns) {
250
+ const clean = pat.replace(/^\*\*\//, '').replace(/\*\*/g, '');
251
+ if (!clean) continue;
252
+ if (rel === pat || rel.startsWith(pat + path.sep) || rel.includes(path.sep + pat + path.sep)) return true;
253
+ if (pat.endsWith('*') && rel.startsWith(pat.slice(0, -1))) return true;
254
+ // Simple glob: *.log
255
+ if (pat.startsWith('*.') && rel.endsWith(pat.slice(1))) return true;
256
+ }
257
+ return false;
258
+ }
259
+
260
+ function walkFiles(cwd, gitPatterns, out = []) {
261
+ const walk = (dir) => {
262
+ let entries;
263
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
264
+ catch { return; }
265
+ for (const entry of entries) {
266
+ const full = path.join(dir, entry.name);
267
+ const rel = path.relative(cwd, full);
268
+ if (isIgnored(rel, gitPatterns)) continue;
269
+ if (entry.isDirectory()) walk(full);
270
+ else if (entry.isFile()) out.push({ full, rel, base: entry.name });
271
+ }
272
+ };
273
+ walk(cwd);
274
+ return out;
275
+ }
276
+
277
+ // ─── Extract helpers ─────────────────────────────────────────────────────────
278
+
279
+ function sha1(text) {
280
+ return crypto.createHash('sha1').update(text).digest('hex').slice(0, 12);
281
+ }
282
+
283
+ function slugify(name) {
284
+ return String(name)
285
+ .replace(/\.[^.]+$/, '')
286
+ .replace(/[^a-zA-Z0-9-_]+/g, '-')
287
+ .replace(/^-+|-+$/g, '')
288
+ .slice(0, 80) || 'note';
289
+ }
290
+
291
+ function extractSnippet(fullPath) {
292
+ let raw = '';
293
+ try {
294
+ const stat = fs.statSync(fullPath);
295
+ if (stat.size > MAX_FILE_BYTES) {
296
+ raw = fs.readFileSync(fullPath, 'utf8').slice(0, MAX_FILE_BYTES);
297
+ } else {
298
+ raw = fs.readFileSync(fullPath, 'utf8');
299
+ }
300
+ } catch {
301
+ return { lines: [], imports: [], exports: [], figma: [] };
302
+ }
303
+ const lines = raw.split('\n').slice(0, MAX_EXTRACT_LINES);
304
+ const imports = [];
305
+ const exports = [];
306
+ const figma = [];
307
+ for (const line of raw.split('\n').slice(0, 200)) {
308
+ const imp = line.match(/from\s+['"]([^'"]+)['"]/);
309
+ if (imp) imports.push(imp[1]);
310
+ if (/^export\s+(default\s+)?(function|const|class|async)/.test(line.trim()) ||
311
+ /^export\s*\{/.test(line.trim())) {
312
+ exports.push(line.trim().slice(0, 120));
313
+ }
314
+ const fig = line.match(/https?:\/\/(?:www\.)?figma\.com\/[^\s)'"]+/gi);
315
+ if (fig) figma.push(...fig);
316
+ }
317
+ return { lines, imports: [...new Set(imports)].slice(0, 20), exports: exports.slice(0, 15), figma: [...new Set(figma)].slice(0, 5) };
318
+ }
319
+
320
+ function classifyFile(rel, base) {
321
+ for (const c of CLASSIFIERS) {
322
+ if (c.match(rel, base)) return c;
323
+ }
324
+ return null;
325
+ }
326
+
327
+ function frontmatter(meta) {
328
+ const lines = ['---'];
329
+ for (const [k, v] of Object.entries(meta)) {
330
+ if (Array.isArray(v)) lines.push(`${k}: [${v.map((x) => JSON.stringify(x)).join(', ')}]`);
331
+ else if (typeof v === 'boolean') lines.push(`${k}: ${v}`);
332
+ else lines.push(`${k}: ${JSON.stringify(String(v))}`);
333
+ }
334
+ lines.push('---');
335
+ return lines.join('\n');
336
+ }
337
+
338
+ function buildDraftNote({ title, rel, classifier, extract, related }) {
339
+ const date = new Date().toISOString().split('T')[0];
340
+ const contentHash = sha1(extract.lines.join('\n'));
341
+ const fm = frontmatter({
342
+ title,
343
+ tags: classifier.tags,
344
+ source: rel,
345
+ category: classifier.category,
346
+ collected_at: date,
347
+ content_hash: contentHash,
348
+ enriched: false,
349
+ });
350
+ const relatedBlock = related.length
351
+ ? related.map((r) => `- [[${r}]]`).join('\n')
352
+ : '- _(none yet)_';
353
+ const body = [
354
+ '## Purpose',
355
+ `Draft extract from \`${rel}\`. Awaiting Kavi enrichment.`,
356
+ '',
357
+ '## Raw extract',
358
+ '```',
359
+ extract.lines.join('\n').slice(0, 3000),
360
+ '```',
361
+ '',
362
+ '## Exports',
363
+ extract.exports.length ? extract.exports.map((e) => `- \`${e}\``).join('\n') : '- _(none detected)_',
364
+ '',
365
+ '## Imports',
366
+ extract.imports.length ? extract.imports.map((i) => `- \`${i}\``).join('\n') : '- _(none detected)_',
367
+ '',
368
+ ...(extract.figma.length ? ['## Figma links', ...extract.figma.map((u) => `- ${u}`), ''] : []),
369
+ '## Related',
370
+ relatedBlock,
371
+ '',
372
+ ].join('\n');
373
+ return { fm, body, contentHash };
374
+ }
375
+
376
+ // ─── Scan → draft vault ──────────────────────────────────────────────────────
377
+
378
+ function scanAndDraft({ cwd, vaultPath, dryRun }) {
379
+ const gitPatterns = loadGitignore(cwd);
380
+ const files = walkFiles(cwd, gitPatterns);
381
+ const notes = []; // { vaultRel, fullOut, title, classifier, contentHash, draft }
382
+
383
+ // First pass: classify + extract
384
+ const candidates = [];
385
+ for (const f of files) {
386
+ const classifier = classifyFile(f.rel, f.base);
387
+ if (!classifier) continue;
388
+ const extract = extractSnippet(f.full);
389
+ const title = slugify(f.base);
390
+ candidates.push({ ...f, classifier, extract, title });
391
+ }
392
+
393
+ // Build simple related links: Components ↔ Pages by shared basename / import
394
+ const pageTitles = candidates.filter((c) => c.classifier.folder === 'Pages').map((c) => `Pages/${c.title}`);
395
+ const componentTitles = candidates.filter((c) => c.classifier.folder === 'Components').map((c) => `Components/${c.title}`);
396
+
397
+ for (const c of candidates) {
398
+ let related = [];
399
+ if (c.classifier.folder === 'Components') {
400
+ related = pageTitles.filter((p) => {
401
+ const pageName = p.split('/')[1].toLowerCase();
402
+ return c.extract.imports.some((i) => i.toLowerCase().includes(c.title.toLowerCase())) ||
403
+ pageName.includes(c.title.toLowerCase().slice(0, 6));
404
+ }).slice(0, 5);
405
+ } else if (c.classifier.folder === 'Pages') {
406
+ related = componentTitles.filter((comp) => {
407
+ const name = comp.split('/')[1].toLowerCase();
408
+ return c.extract.imports.some((i) => i.toLowerCase().includes(name));
409
+ }).slice(0, 8);
410
+ }
411
+
412
+ const { fm, body, contentHash } = buildDraftNote({
413
+ title: c.title,
414
+ rel: c.rel,
415
+ classifier: c.classifier,
416
+ extract: c.extract,
417
+ related,
418
+ });
419
+
420
+ // Disambiguate colliding titles within a folder
421
+ let vaultRel = path.join(c.classifier.folder, `${c.title}.md`);
422
+ let n = 2;
423
+ while (notes.some((x) => x.vaultRel === vaultRel)) {
424
+ vaultRel = path.join(c.classifier.folder, `${c.title}-${n}.md`);
425
+ n += 1;
426
+ }
427
+
428
+ notes.push({
429
+ vaultRel,
430
+ fullOut: path.join(vaultPath, vaultRel),
431
+ title: c.title,
432
+ classifier: c.classifier,
433
+ contentHash,
434
+ draft: `${fm}\n\n${body}`,
435
+ sourceRel: c.rel,
436
+ });
437
+ }
438
+
439
+ if (!dryRun) {
440
+ fs.mkdirSync(vaultPath, { recursive: true });
441
+ for (const note of notes) {
442
+ // Hash-based skip: if existing note has same content_hash, keep enriched body
443
+ if (fs.existsSync(note.fullOut)) {
444
+ try {
445
+ const existing = fs.readFileSync(note.fullOut, 'utf8');
446
+ const m = existing.match(/content_hash:\s*"?([a-f0-9]+)"?/);
447
+ const enriched = /enriched:\s*true/.test(existing);
448
+ if (m && m[1] === note.contentHash && enriched) {
449
+ note.skipped = true;
450
+ note.draft = existing; // keep previous enriched version
451
+ continue;
452
+ }
453
+ } catch { /* rewrite */ }
454
+ }
455
+ fs.mkdirSync(path.dirname(note.fullOut), { recursive: true });
456
+ fs.writeFileSync(note.fullOut, note.draft);
457
+ }
458
+ writeMeta(vaultPath, notes, cwd);
459
+ }
460
+
461
+ return {
462
+ filesScanned: files.length,
463
+ notesWritten: notes.filter((n) => !n.skipped).length,
464
+ notesSkipped: notes.filter((n) => n.skipped).length,
465
+ notes,
466
+ };
467
+ }
468
+
469
+ function writeMeta(vaultPath, notes, cwd) {
470
+ const byFolder = {};
471
+ for (const n of notes) {
472
+ const folder = n.classifier.folder;
473
+ if (!byFolder[folder]) byFolder[folder] = [];
474
+ byFolder[folder].push(n);
475
+ }
476
+
477
+ const indexLines = [
478
+ '---',
479
+ 'title: "Knowledge Vault Index"',
480
+ 'tags: [meta, moc]',
481
+ `collected_at: "${new Date().toISOString().split('T')[0]}"`,
482
+ '---',
483
+ '',
484
+ '# Knowledge Vault Index',
485
+ '',
486
+ `Source repo: \`${cwd}\``,
487
+ '',
488
+ '## Map of content',
489
+ '',
490
+ ];
491
+ for (const [folder, list] of Object.entries(byFolder).sort()) {
492
+ indexLines.push(`### ${folder}`, '');
493
+ for (const n of list) {
494
+ const link = n.vaultRel.replace(/\.md$/, '').replace(/\\/g, '/');
495
+ indexLines.push(`- [[${link}|${n.title}]] — \`${n.sourceRel}\``);
496
+ }
497
+ indexLines.push('');
498
+ }
499
+ fs.mkdirSync(path.join(vaultPath, '_meta'), { recursive: true });
500
+ fs.writeFileSync(path.join(vaultPath, '_meta', 'index.md'), indexLines.join('\n'));
501
+ fs.writeFileSync(path.join(vaultPath, '_meta', 'last-sync.md'), [
502
+ '---',
503
+ 'title: "Last sync"',
504
+ '---',
505
+ '',
506
+ `# Last sync`,
507
+ '',
508
+ `- **When:** ${new Date().toISOString()}`,
509
+ `- **Repo:** ${cwd}`,
510
+ `- **Notes:** ${notes.length}`,
511
+ '',
512
+ ].join('\n'));
513
+ }
514
+
515
+ // ─── LLM enrichment ──────────────────────────────────────────────────────────
516
+
517
+ function loadKaviCard() {
518
+ const p = path.join(PACKAGE_ROOT, 'agents', 'cards', 'kavi.md');
519
+ if (fs.existsSync(p)) return fs.readFileSync(p, 'utf8');
520
+ return 'You are Kavi, the Knowledge Archivist. Rewrite draft extracts into concise Obsidian notes. Do not invent files or APIs.';
521
+ }
522
+
523
+ function hasAnyApiKey() {
524
+ return !!(
525
+ process.env.ANTHROPIC_API_KEY ||
526
+ process.env.OPENAI_API_KEY ||
527
+ process.env.GEMINI_API_KEY ||
528
+ process.env.GOOGLE_API_KEY ||
529
+ process.env.ZAI_API_KEY ||
530
+ process.env.ZHIPU_API_KEY
531
+ );
532
+ }
533
+
534
+ function pickEnrichProvider(config) {
535
+ // Prefer cheap structured tier: openai mini → google flash → anthropic → zai
536
+ if (process.env.OPENAI_API_KEY) return { provider: 'openai', model: config.orchestrator?.tiers?.structured?.model || 'gpt-4o-mini' };
537
+ if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) return { provider: 'google', model: 'gemini-2.5-flash' };
538
+ if (process.env.ZAI_API_KEY || process.env.ZHIPU_API_KEY) return { provider: 'zai', model: 'glm-4.5-flash' };
539
+ if (process.env.ANTHROPIC_API_KEY) return { provider: 'anthropic', model: 'claude-haiku-4-5-20251001' };
540
+ return null;
541
+ }
542
+
543
+ function parseEnrichedBatch(text, count) {
544
+ // Expect notes separated by ---NOTE N--- markers; fall back to whole text for single note
545
+ const parts = text.split(/---NOTE\s+\d+---/i).map((s) => s.trim()).filter(Boolean);
546
+ if (parts.length >= count) return parts.slice(0, count);
547
+ if (parts.length === 1 && count === 1) return parts;
548
+ // Uneven response — pad with nulls so we keep drafts for missing ones
549
+ while (parts.length < count) parts.push(null);
550
+ return parts.slice(0, count);
551
+ }
552
+
553
+ async function enrichNotes({ notes, limit, provider, model }) {
554
+ const { callLlm, loadConfig } = require('./orchestrator/run');
555
+ const config = loadConfig();
556
+ const chosen = provider && model
557
+ ? { provider, model }
558
+ : pickEnrichProvider(config);
559
+ if (!chosen) {
560
+ return { enriched: 0, skipped: notes.length, reason: 'No API key set — wrote draft-only vault.', llm_calls: 0, input_tokens_est: 0, output_tokens_est: 0 };
561
+ }
562
+
563
+ const toEnrich = notes.filter((n) => !n.skipped).slice(0, limit == null ? notes.length : limit);
564
+ const system = [
565
+ loadKaviCard(),
566
+ '',
567
+ 'For each draft note below, rewrite ONLY the body (keep the YAML frontmatter I give you).',
568
+ 'Output format: for each note, emit exactly:',
569
+ '---NOTE 1---',
570
+ '<full markdown including the original frontmatter with enriched: true>',
571
+ '---NOTE 2---',
572
+ '...',
573
+ 'Rules: purpose + key facts + Related wikilinks. Do NOT invent files, APIs, or components not in the Raw extract.',
574
+ ].join('\n');
575
+
576
+ let enriched = 0;
577
+ let llmCalls = 0;
578
+ let inTok = 0;
579
+ let outTok = 0;
580
+
581
+ for (let i = 0; i < toEnrich.length; i += BATCH_SIZE) {
582
+ const batch = toEnrich.slice(i, i + BATCH_SIZE);
583
+ const user = batch.map((n, idx) => `---NOTE ${idx + 1}---\n${n.draft}`).join('\n\n');
584
+ const text = await callLlm({
585
+ provider: chosen.provider,
586
+ model: chosen.model,
587
+ system,
588
+ user,
589
+ maxTokens: 4000,
590
+ });
591
+ llmCalls += 1;
592
+ inTok += Math.ceil((system.length + user.length) / 4);
593
+ outTok += Math.ceil(text.length / 4);
594
+
595
+ const parts = parseEnrichedBatch(text, batch.length);
596
+ for (let j = 0; j < batch.length; j++) {
597
+ let body = parts[j];
598
+ if (!body) continue;
599
+ // Ensure enriched: true in frontmatter
600
+ if (!/enriched:\s*true/.test(body)) {
601
+ body = body.replace(/enriched:\s*false/, 'enriched: true');
602
+ if (!/enriched:/.test(body) && body.startsWith('---')) {
603
+ body = body.replace(/^---\n/, '---\nenriched: true\n');
604
+ }
605
+ }
606
+ // Preserve content_hash from draft
607
+ if (!/content_hash:/.test(body) && batch[j].contentHash) {
608
+ body = body.replace(/^---\n/, `---\ncontent_hash: "${batch[j].contentHash}"\n`);
609
+ }
610
+ fs.writeFileSync(batch[j].fullOut, body);
611
+ batch[j].draft = body;
612
+ enriched += 1;
613
+ }
614
+ }
615
+
616
+ return {
617
+ enriched,
618
+ skipped: notes.length - enriched,
619
+ reason: null,
620
+ llm_calls: llmCalls,
621
+ input_tokens_est: inTok,
622
+ output_tokens_est: outTok,
623
+ provider: chosen.provider,
624
+ model: chosen.model,
625
+ };
626
+ }
627
+
628
+ // ─── Public API ──────────────────────────────────────────────────────────────
629
+
630
+ /**
631
+ * @param {{
632
+ * cwd?: string,
633
+ * vault?: string,
634
+ * project?: string,
635
+ * dryRun?: boolean,
636
+ * enrich?: boolean,
637
+ * limit?: number,
638
+ * target?: string,
639
+ * provider?: string,
640
+ * model?: string,
641
+ * web?: boolean,
642
+ * webLimit?: number,
643
+ * }} opts
644
+ */
645
+ async function collect(opts = {}) {
646
+ const cwd = path.resolve(opts.cwd || process.cwd());
647
+ const projectId = opts.project || session.getProjectId(cwd);
648
+ const vaultPath = path.resolve(opts.vault || path.join(os.homedir(), '.analyzthis_design', 'vaults', projectId));
649
+ const enrich = opts.enrich !== false;
650
+ const dryRun = !!opts.dryRun;
651
+ const limit = opts.limit != null ? opts.limit : null;
652
+ const fetchWeb = opts.web !== false;
653
+ const discoverSources = opts.discoverSources !== false;
654
+ const config = loadCollectConfig();
655
+ const { resolveTargets } = require('./platforms');
656
+ const targets = resolveTargets(opts.target || 'cursor') || ['cursor'];
657
+
658
+ // Ensure session exists
659
+ if (!session.show({ project: projectId })) {
660
+ session.init({ project: projectId });
661
+ }
662
+
663
+ const modeParts = [];
664
+ if (dryRun) modeParts.push('dry-run');
665
+ else {
666
+ if (discoverSources) modeParts.push('discover');
667
+ if (fetchWeb) modeParts.push('web');
668
+ if (enrich) modeParts.push('enrich');
669
+ modeParts.push('sync');
670
+ }
671
+
672
+ console.log(`\n📚 Kavi — Collect Knowledge`);
673
+ console.log(` Repo: ${cwd}`);
674
+ console.log(` Vault: ${vaultPath}`);
675
+ console.log(` Mode: ${modeParts.join(' + ') || 'scan'}\n`);
676
+
677
+ const scan = scanAndDraft({ cwd, vaultPath, dryRun });
678
+
679
+ const sourceDiscoveries = discoverSources
680
+ ? discoverKnowledgeSources({ cwd, config })
681
+ : [];
682
+
683
+ console.log(` Scanned ${scan.filesScanned} file(s) → ${scan.notes.length} note(s) (${scan.notesWritten} written, ${scan.notesSkipped} unchanged)`);
684
+
685
+ if (discoverSources && sourceDiscoveries.length > 0) {
686
+ console.log(`\n🔍 Knowledge sources: ${sourceDiscoveries.length} discovered (Obsidian vaults, wikis, referenced paths)`);
687
+ for (const d of sourceDiscoveries.slice(0, 6)) {
688
+ console.log(` • [${d.confidence}] ${d.type}: ${d.path}`);
689
+ }
690
+ if (sourceDiscoveries.length > 6) console.log(` … and ${sourceDiscoveries.length - 6} more`);
691
+ } else if (discoverSources && !dryRun) {
692
+ console.log(`\n🔍 Knowledge sources: none discovered (add collect.source_paths in config or link vaults in README)`);
693
+ }
694
+
695
+ const webPlan = fetchWeb
696
+ ? discoverWebUrls({ cwd, notes: scan.notes, config, webLimit: opts.webLimit })
697
+ : { urls: [], queries: [], limit: 0, totalDiscovered: 0 };
698
+
699
+ if (fetchWeb && (webPlan.urls.length > 0 || webPlan.queries.length > 0)) {
700
+ console.log(`\n🌐 Web research: ${webPlan.urls.length} URL(s)${webPlan.queries.length ? `, ${webPlan.queries.length} query stub(s)` : ''}${webPlan.totalDiscovered > webPlan.urls.length ? ` (${webPlan.totalDiscovered} found, capped at ${webPlan.limit})` : ''}`);
701
+ } else if (fetchWeb && !dryRun) {
702
+ console.log(`\n🌐 Web research: no URLs found (add collect.web_urls in ~/.analyzthis_design/config.json or links in PRDs/README)`);
703
+ }
704
+
705
+ if (dryRun) {
706
+ const byFolder = {};
707
+ for (const n of scan.notes) {
708
+ byFolder[n.classifier.folder] = (byFolder[n.classifier.folder] || 0) + 1;
709
+ }
710
+ console.log('\n── Dry run preview ──');
711
+ console.log(JSON.stringify({
712
+ vaultPath,
713
+ projectId,
714
+ byFolder,
715
+ knowledgeSources: sourceDiscoveries.map((d) => ({
716
+ path: d.path,
717
+ type: d.type,
718
+ confidence: d.confidence,
719
+ discovered_from: d.discovered_from,
720
+ markdown_files: d.stats.markdown_files,
721
+ })),
722
+ webUrls: webPlan.urls.map((u) => ({ url: u.url, source: u.source })),
723
+ webQueries: webPlan.queries,
724
+ sample: scan.notes.slice(0, 8).map((n) => n.vaultRel),
725
+ }, null, 2));
726
+ console.log('');
727
+ return { vaultPath, projectId, dryRun: true, ...scan, sources: sourceDiscoveries, web: webPlan, enrich: null, sync: null };
728
+ }
729
+
730
+ let sourceManifest = { written: [] };
731
+ if (discoverSources && sourceDiscoveries.length > 0) {
732
+ console.log(`\n⏳ Writing source manifest notes (Sources/*.md)...`);
733
+ sourceManifest = writeSourceManifestNotes({
734
+ discoveries: sourceDiscoveries,
735
+ vaultPath,
736
+ dryRun: false,
737
+ });
738
+ console.log(` Wrote ${sourceManifest.written.length} manifest note(s) + _meta/knowledge-sources.md`);
739
+ }
740
+
741
+ let webResult = { fetched: [], failed: [], queries: [] };
742
+ if (fetchWeb && (webPlan.urls.length > 0 || webPlan.queries.length > 0)) {
743
+ console.log(`\n⏳ Fetching web research into web-context.md...`);
744
+ webResult = await collectWebResearch({
745
+ urls: webPlan.urls,
746
+ queries: webPlan.queries,
747
+ projectId,
748
+ dryRun: false,
749
+ });
750
+ console.log(` Fetched ${webResult.fetched.length} URL(s)${webResult.failed.length ? `, ${webResult.failed.length} failed` : ''}`);
751
+ }
752
+
753
+ let enrichResult = { enriched: 0, skipped: scan.notes.length, reason: 'enrichment disabled', llm_calls: 0, input_tokens_est: 0, output_tokens_est: 0 };
754
+ if (enrich) {
755
+ if (!hasAnyApiKey()) {
756
+ console.log(`\n ⚠ No LLM API key found — writing draft-only vault. Set OPENAI_API_KEY / GEMINI_API_KEY / ANTHROPIC_API_KEY / ZAI_API_KEY to enrich.`);
757
+ enrichResult.reason = 'No API key set — wrote draft-only vault.';
758
+ } else {
759
+ console.log(`\n⏳ Enriching notes with Kavi (LLM)...`);
760
+ enrichResult = await enrichNotes({
761
+ notes: scan.notes,
762
+ limit,
763
+ provider: opts.provider,
764
+ model: opts.model,
765
+ });
766
+ console.log(` Enriched ${enrichResult.enriched} note(s)${enrichResult.reason ? ` (${enrichResult.reason})` : ''}`);
767
+ }
768
+ }
769
+
770
+ // Auto-connect discovered sources + Kavi vault, then sync knowledge bank
771
+ console.log(`\n⏳ Wiring knowledge bank...`);
772
+ const connectResult = discoverSources
773
+ ? connectDiscoveredSources({
774
+ discoveries: sourceDiscoveries,
775
+ kaviVaultPath: vaultPath,
776
+ config,
777
+ autoConnect: config.collect?.auto_connect_discovered !== false,
778
+ })
779
+ : { connected: [], skipped: [] };
780
+ if (connectResult.connected.length > 0) {
781
+ console.log(` Auto-connected ${connectResult.connected.length} source(s):`);
782
+ for (const p of connectResult.connected) console.log(` • ${p}`);
783
+ }
784
+ knowledge.connect({ vaultPath, tags: [], include: [] });
785
+ const syncResult = knowledge.sync({ targets });
786
+ cache.invalidatePrefix('kb:');
787
+
788
+ if (syncResult.message) {
789
+ console.log(` ⚠ ${syncResult.message}`);
790
+ } else {
791
+ console.log(`✅ Synced ${syncResult.synced} note(s) into the knowledge bank.`);
792
+ for (const t of syncResult.copiedTo || []) console.log(` • ${t}`);
793
+ }
794
+
795
+ // Persist session fields
796
+ const state = session.show({ project: projectId }) || {};
797
+ const sources = Array.isArray(state.content_sources) ? state.content_sources : [];
798
+ if (!sources.includes('codebase_collect')) sources.push('codebase_collect');
799
+ session.update({
800
+ project: projectId,
801
+ patch: {
802
+ content_sources: sources,
803
+ vault_path: vaultPath,
804
+ last_collect_at: new Date().toISOString(),
805
+ digest: {
806
+ ...(state.digest || {}),
807
+ task_map_summary: `Collected knowledge vault (${scan.notes.length} notes) from ${cwd}`,
808
+ experts: ['kavi'],
809
+ },
810
+ metrics: {
811
+ ...(state.metrics || {}),
812
+ llm_calls: enrichResult.llm_calls || 0,
813
+ experts_run: ['kavi'],
814
+ input_tokens_est: enrichResult.input_tokens_est || 0,
815
+ output_tokens_est: enrichResult.output_tokens_est || 0,
816
+ mode: 'collect',
817
+ effort_log: enrichResult.model
818
+ ? [{ persona: 'kavi', effort: 'standard', model: enrichResult.model, scoped_mode: null }]
819
+ : [],
820
+ cost_usd: state.metrics?.cost_usd || 0,
821
+ },
822
+ },
823
+ });
824
+
825
+ console.log(`\n✅ Collect complete.`);
826
+ console.log(` Vault: ${vaultPath}`);
827
+ console.log(` Open in Obsidian, or run /persona-orchestrator — personas now read this knowledge bank.\n`);
828
+
829
+ return {
830
+ vaultPath,
831
+ projectId,
832
+ dryRun: false,
833
+ filesScanned: scan.filesScanned,
834
+ notes: scan.notes.length,
835
+ notesWritten: scan.notesWritten,
836
+ notesSkipped: scan.notesSkipped,
837
+ web: webResult,
838
+ sources: {
839
+ discovered: sourceDiscoveries,
840
+ manifest: sourceManifest,
841
+ connected: connectResult.connected,
842
+ skipped: connectResult.skipped,
843
+ },
844
+ enrich: enrichResult,
845
+ sync: syncResult,
846
+ };
847
+ }
848
+
849
+ module.exports = {
850
+ collect,
851
+ scanAndDraft,
852
+ classifyFile,
853
+ walkFiles,
854
+ discoverWebUrls,
855
+ collectWebResearch,
856
+ extractUrlsFromText,
857
+ isResearchableUrl,
858
+ HARD_EXCLUDES,
859
+ };