1(function () { 2 const modelContext = navigator.modelContext; 3 if (!modelContext || typeof modelContext.registerTool !== 'function') { 4 return; 5 } 6 7 const controller = new AbortController(); 8 const sections = { 9 home: '/', 10 about: '/about', 11 projects: '/projects', 12 blog: '/blog', 13 api_docs: '/docs/api', 14 llms: '/llms.txt' 15 }; 16 17 function absolute(pathname) { 18 return new URL(pathname, window.location.origin).toString(); 19 } 20 21 function registerTool(definition) { 22 modelContext.registerTool({ 23 ...definition, 24 signal: controller.signal 25 }); 26 } 27 28 registerTool({ 29 name: 'list_site_sections', 30 description: 'List the main yaqeen.me sections and discovery resources.', 31 inputSchema: { 32 type: 'object', 33 properties: {}, 34 additionalProperties: false 35 }, 36 execute: async function () { 37 return { 38 content: [ 39 { 40 type: 'text', 41 text: JSON.stringify( 42 Object.entries(sections).map(function ([name, pathname]) { 43 return { name: name, url: absolute(pathname) }; 44 }), 45 null, 46 2 47 ) 48 } 49 ] 50 }; 51 } 52 }); 53 54 registerTool({ 55 name: 'search_blog_posts', 56 description: 'Search yaqeen.me blog posts by title, slug, and description.', 57 inputSchema: { 58 type: 'object', 59 properties: { 60 query: { type: 'string', minLength: 1 }, 61 limit: { type: 'integer', minimum: 1, maximum: 10, default: 5 } 62 }, 63 required: ['query'], 64 additionalProperties: false 65 }, 66 execute: async function (input) { 67 const query = String(input.query || '') 68 .trim() 69 .toLowerCase(); 70 const limit = Math.min(Math.max(Number(input.limit) || 5, 1), 10); 71 72 const response = await fetch('/blog/api/posts?limit=100'); 73 if (!response.ok) { 74 throw new Error('Unable to load blog posts.'); 75 } 76 77 const data = await response.json(); 78 const matches = (data.allPosts || []) 79 .filter(function (post) { 80 const haystack = [post.title, post.slug, post.description].join(' ').toLowerCase(); 81 return haystack.includes(query); 82 }) 83 .slice(0, limit) 84 .map(function (post) { 85 return { 86 title: post.title, 87 description: post.description, 88 url: post.canonicalUrl || absolute(post.href), 89 isExternal: Boolean(post.isExternal) 90 }; 91 }); 92 93 return { 94 content: [ 95 { 96 type: 'text', 97 text: JSON.stringify(matches, null, 2) 98 } 99 ] 100 }; 101 } 102 }); 103 104 registerTool({ 105 name: 'get_current_page_context', 106 description: 'Return the current yaqeen.me page title, URL, and description.', 107 inputSchema: { 108 type: 'object', 109 properties: {}, 110 additionalProperties: false 111 }, 112 execute: async function () { 113 const descriptionNode = document.querySelector('meta[name="description"]'); 114 115 return { 116 content: [ 117 { 118 type: 'text', 119 text: JSON.stringify( 120 { 121 title: document.title, 122 url: window.location.href, 123 description: descriptionNode ? descriptionNode.getAttribute('content') : '' 124 }, 125 null, 126 2 127 ) 128 } 129 ] 130 }; 131 } 132 }); 133 134 window.addEventListener( 135 'pagehide', 136 function () { 137 controller.abort(); 138 }, 139 { once: true } 140 ); 141})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.