@karmaniverous/jeeves-watcher 0.2.0 → 0.2.2
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/dist/cjs/index.js +537 -310
- package/dist/cli/jeeves-watcher/index.js +659 -404
- package/dist/index.d.ts +104 -56
- package/dist/index.iife.js +536 -309
- package/dist/index.iife.min.js +1 -1
- package/dist/mjs/index.js +538 -311
- package/package.json +1 -1
package/dist/index.iife.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(e,t,i,r,n,o,a,s,c,l,d,h,u,g,f,m,p,y,w,b,v){"use strict";function M(e){var t=Object.create(null);return e&&Object.keys(e).forEach((function(i){if("default"!==i){var r=Object.getOwnPropertyDescriptor(e,i);Object.defineProperty(t,i,r.get?r:{enumerable:!0,get:function(){return e[i]}})}})),t.default=e,Object.freeze(t)}var P=M(f);function k(e,t){const i=function(e){return e.replace(/\\/g,"/").replace(/^([A-Za-z]):/,((e,t)=>t.toLowerCase())).toLowerCase()}(e),n=r.createHash("sha256").update(i,"utf8").digest("hex");return o.join(t,`${n}.meta.json`)}async function x(e,t){try{const i=await n.readFile(k(e,t),"utf8");return JSON.parse(i)}catch{return null}}async function S(e,t,i){const r=k(e,t);await n.mkdir(o.dirname(r),{recursive:!0}),await n.writeFile(r,JSON.stringify(i,null,2),"utf8")}async function z(e,t){try{await n.rm(k(e,t))}catch{}}function j(e){const t=e.replace(/\\/g,"/"),i=t.search(/[*?\[]/);if(-1===i)return o.resolve(e);const r=t.slice(0,i),n=r.endsWith("/")?r.slice(0,-1):o.dirname(r);return o.resolve(n)}async function*C(e){let t;try{t=(await n.readdir(e,{withFileTypes:!0})).map((e=>({name:e.name,isDirectory:e.isDirectory()})))}catch{return}for(const i of t){const t=o.resolve(e,i.name);if(i.isDirectory)yield*C(t);else try{(await n.stat(t)).isFile()&&(yield t)}catch{}}}async function R(e,t,i,r){const n=await async function(e,t=[]){const i=e.map((e=>e.replace(/\\/g,"/"))),r=t.map((e=>e.replace(/\\/g,"/"))),n=a(i,{dot:!0}),o=r.length?a(r,{dot:!0}):()=>!1,s=Array.from(new Set(e.map(j))),c=new Set;for(const e of s)for await(const t of C(e)){const e=t.replace(/\\/g,"/");o(e)||n(e)&&c.add(t)}return Array.from(c)}(e,t);for(const e of n)await i[r](e);return n.length}function F(e){const{processor:r,vectorStore:n,embeddingProvider:o,logger:a}=e,s=t({logger:!1});return s.get("/status",(()=>({status:"ok",uptime:process.uptime()}))),s.post("/metadata",(async(e,t)=>{try{const{path:t,metadata:i}=e.body;return await r.processMetadataUpdate(t,i),{ok:!0}}catch(e){return a.error({error:e},"Metadata update failed"),t.status(500).send({error:"Internal server error"})}})),s.post("/search",(async(e,t)=>{try{const{query:t,limit:i=10}=e.body,r=await o.embed([t]);return await n.search(r[0],i)}catch(e){return a.error({error:e},"Search failed"),t.status(500).send({error:"Internal server error"})}})),s.post("/reindex",(async(t,i)=>{try{const t=await R(e.config.watch.paths,e.config.watch.ignored,r,"processFile");return await i.status(200).send({ok:!0,filesIndexed:t})}catch(e){return a.error({error:e},"Reindex failed"),await i.status(500).send({error:"Internal server error"})}})),s.post("/rebuild-metadata",(async(t,r)=>{try{const t=e.config.metadataDir??".jeeves-metadata",o=["file_path","chunk_index","total_chunks","content_hash","chunk_text"];for await(const e of n.scroll()){const r=e.payload,n=r.file_path;if("string"!=typeof n||0===n.length)continue;const a=i.omit(r,o);await S(n,t,a)}return await r.status(200).send({ok:!0})}catch(e){return a.error({error:e},"Rebuild metadata failed"),await r.status(500).send({error:"Internal server error"})}})),s.post("/config-reindex",(async(t,i)=>{try{const n=t.body.scope??"rules";return(async()=>{try{if("rules"===n){const t=await R(e.config.watch.paths,e.config.watch.ignored,r,"processRulesUpdate");a.info({scope:n,filesProcessed:t},"Config reindex (rules) completed")}else{const t=await R(e.config.watch.paths,e.config.watch.ignored,r,"processFile");a.info({scope:n,filesProcessed:t},"Config reindex (full) completed")}}catch(e){a.error({error:e,scope:n},"Config reindex failed")}})(),await i.status(200).send({status:"started",scope:n})}catch(e){return a.error({error:e},"Config reindex request failed"),await i.status(500).send({error:"Internal server error"})}})),s}const T={metadataDir:".jeeves-watcher",shutdownTimeoutMs:1e4},D={enabled:!0,debounceMs:1e3},I={host:"127.0.0.1",port:3456},E={level:"info"},q={debounceMs:300,stabilityThresholdMs:500,usePolling:!1,pollIntervalMs:1e3},N={chunkSize:1e3,chunkOverlap:200,dimensions:3072,rateLimitPerMinute:300,concurrency:5},W=l.z.object({paths:l.z.array(l.z.string()).min(1).describe('Glob patterns for files to watch (e.g., "**/*.md"). At least one required.'),ignored:l.z.array(l.z.string()).optional().describe('Glob patterns to exclude from watching (e.g., "**/node_modules/**").'),pollIntervalMs:l.z.number().optional().describe("Polling interval in milliseconds when usePolling is enabled."),usePolling:l.z.boolean().optional().describe("Use polling instead of native file system events (for network drives)."),debounceMs:l.z.number().optional().describe("Debounce delay in milliseconds for file change events."),stabilityThresholdMs:l.z.number().optional().describe("Time in milliseconds a file must remain unchanged before processing.")}),_=l.z.object({enabled:l.z.boolean().optional().describe("Enable automatic reloading when config file changes."),debounceMs:l.z.number().optional().describe("Debounce delay in milliseconds for config file change detection.")}),A=l.z.object({provider:l.z.string().default("gemini").describe('Embedding provider name (e.g., "gemini", "openai").'),model:l.z.string().default("gemini-embedding-001").describe('Embedding model identifier (e.g., "gemini-embedding-001", "text-embedding-3-small").'),chunkSize:l.z.number().optional().describe("Maximum chunk size in characters for text splitting."),chunkOverlap:l.z.number().optional().describe("Character overlap between consecutive chunks."),dimensions:l.z.number().optional().describe("Embedding vector dimensions (must match model output)."),apiKey:l.z.string().optional().describe("API key for embedding provider (supports ${ENV_VAR} substitution)."),rateLimitPerMinute:l.z.number().optional().describe("Maximum embedding API requests per minute (rate limiting)."),concurrency:l.z.number().optional().describe("Maximum concurrent embedding requests.")}),Q=l.z.object({url:l.z.string().describe('Qdrant server URL (e.g., "http://localhost:6333").'),collectionName:l.z.string().describe("Qdrant collection name for vector storage."),apiKey:l.z.string().optional().describe("Qdrant API key for authentication (supports ${ENV_VAR} substitution).")}),L=l.z.object({host:l.z.string().optional().describe('Host address for API server (e.g., "127.0.0.1", "0.0.0.0").'),port:l.z.number().optional().describe("Port for API server (e.g., 3456).")}),O=l.z.object({level:l.z.string().optional().describe("Logging level (trace, debug, info, warn, error, fatal)."),file:l.z.string().optional().describe("Path to log file (logs to stdout if omitted).")}),$=l.z.object({match:l.z.record(l.z.string(),l.z.unknown()).describe("JSON Schema object to match against file attributes."),set:l.z.record(l.z.string(),l.z.unknown()).describe("Metadata fields to set when match succeeds."),map:l.z.union([d.jsonMapMapSchema,l.z.string()]).optional().describe("JsonMap transformation (inline definition or named map reference).")}),K=l.z.object({watch:W.describe("File system watch configuration."),configWatch:_.optional().describe("Configuration file watch settings."),embedding:A.describe("Embedding model configuration."),vectorStore:Q.describe("Qdrant vector store configuration."),metadataDir:l.z.string().optional().describe("Directory for persisted metadata sidecar files."),api:L.optional().describe("API server configuration."),extractors:l.z.record(l.z.string(),l.z.unknown()).optional().describe("Extractor configurations keyed by name."),inferenceRules:l.z.array($).optional().describe("Rules for inferring metadata from file attributes."),maps:l.z.record(l.z.string(),d.jsonMapMapSchema).optional().describe("Reusable named JsonMap transformations."),logging:O.optional().describe("Logging configuration."),shutdownTimeoutMs:l.z.number().optional().describe("Timeout in milliseconds for graceful shutdown.")}),J="jeeves-watcher";async function G(e){const t=c.cosmiconfig(J),i=e?await t.load(e):await t.search();if(!i||i.isEmpty)throw new Error("No jeeves-watcher configuration found. Create a .jeeves-watcherrc or jeeves-watcher.config.{js,ts,json,yaml} file.");try{const e=K.parse(i.config);return r=e,{...T,...r,watch:{...q,...r.watch},configWatch:{...D,...r.configWatch},embedding:{...N,...r.embedding},api:{...I,...r.api},logging:{...E,...r.logging}}}catch(e){if(e instanceof l.ZodError){const t=e.issues.map((e=>`${e.path.join(".")}: ${e.message}`)).join("; ");throw new Error(`Invalid jeeves-watcher configuration: ${t}`)}throw e}var r}function U(e,t){return e<=0?Promise.resolve():new Promise(((i,r)=>{const n=setTimeout((()=>{a(),i()}),e),o=()=>{a(),r(new Error("Retry sleep aborted"))},a=()=>{clearTimeout(n),t&&t.removeEventListener("abort",o)};if(t){if(t.aborted)return void o();t.addEventListener("abort",o,{once:!0})}}))}function B(e,t,i,r=0){const n=Math.max(0,e-1),o=Math.min(i,t*2**n),a=r>0?1+Math.random()*r:1;return Math.round(o*a)}async function V(e,t){const i=Math.max(1,t.attempts);let r;for(let n=1;n<=i;n++)try{return await e(n)}catch(e){r=e;if(n>=i)break;const o=B(n,t.baseDelayMs,t.maxDelayMs,t.jitter);t.onRetry?.({attempt:n,attempts:i,delayMs:o,error:e}),await U(o,t.signal)}throw r}const H=new Map([["mock",function(e){return function(e){return{dimensions:e,embed:t=>Promise.resolve(t.map((t=>{const i=r.createHash("sha256").update(t,"utf8").digest(),n=[];for(let t=0;t<e;t++){const e=i[t%i.length];n.push(e/127.5-1)}return n})))}}(e.dimensions??768)}],["gemini",function(e,t){if(!e.apiKey)throw new Error("Gemini embedding provider requires config.embedding.apiKey");const i=e.dimensions??3072,r=new h.GoogleGenerativeAIEmbeddings({apiKey:e.apiKey,model:e.model});return{dimensions:i,async embed(n){const o=await V((async i=>{if(i>1){const r={attempt:i,provider:"gemini",model:e.model};t?t.warn(r,"Retrying embedding request"):console.warn(r,"Retrying embedding request")}return r.embedDocuments(n)}),{attempts:5,baseDelayMs:500,maxDelayMs:1e4,jitter:.2,onRetry:({attempt:i,delayMs:r,error:n})=>{const o={attempt:i,delayMs:r,provider:"gemini",model:e.model,error:n};t?t.warn(o,"Embedding call failed; will retry"):console.warn(o,"Embedding call failed; will retry")}});for(const e of o)if(e.length!==i)throw new Error(`Gemini embedding returned invalid dimensions: expected ${String(i)}, got ${String(e.length)}`);return o}}}]]);function Z(e,t){const i=H.get(e.provider);if(!i)throw new Error(`Unsupported embedding provider: ${e.provider}`);return i(e,t)}function Y(e){const t=e?.level??"info";if(e?.file){const i=u.transport({target:"pino/file",options:{destination:e.file,mkdir:!0}});return u({level:t},i)}return u({level:t})}function X(e){return r.createHash("sha256").update(e,"utf8").digest("hex")}const ee="6a6f686e-6761-4c74-ad6a-656576657321";function te(e){return e.replace(/\\/g,"/").toLowerCase()}function ie(e,t){const i=void 0!==t?`${te(e)}#${String(t)}`:te(e);return g.v5(i,ee)}const re=["content","body","text","snippet","subject","description","summary","transcript"];function ne(e){if(!e||"object"!=typeof e)return JSON.stringify(e);const t=e;for(const e of re){const i=t[e];if("string"==typeof i&&i.trim())return i}return JSON.stringify(e)}async function oe(e){const t=await n.readFile(e,"utf8"),{frontmatter:i,body:r}=function(e){const t=e.replace(/^\uFEFF/,""),i=/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/m.exec(t);if(!i)return{body:e};const[,r,n]=i,o=m.load(r);return{frontmatter:o&&"object"==typeof o&&!Array.isArray(o)?o:void 0,body:n}}(t);return{text:r,frontmatter:i}}async function ae(e){return{text:await n.readFile(e,"utf8")}}async function se(e){const t=await n.readFile(e,"utf8"),i=P.load(t);i("script, style").remove();return{text:i("body").text().trim()||i.text().trim()}}const ce=new Map([[".md",oe],[".markdown",oe],[".txt",ae],[".text",ae],[".json",async function(e){const t=await n.readFile(e,"utf8"),i=JSON.parse(t),r=i&&"object"==typeof i&&!Array.isArray(i)?i:void 0;return{text:ne(i),json:r}}],[".pdf",async function(e){const t=await n.readFile(e),i=new Uint8Array(t),{extractText:r}=await import("unpdf"),{text:o}=await r(i);return{text:Array.isArray(o)?o.join("\n\n"):o}}],[".docx",async function(e){const t=await n.readFile(e);return{text:(await p.extractRawText({buffer:t})).value}}],[".html",se],[".htm",se]]);async function le(e,t){const i=ce.get(t.toLowerCase());return i?i(e):ae(e)}function de(e,t,i,r){const n=e.replace(/\\/g,"/"),a={file:{path:n,directory:o.dirname(n).replace(/\\/g,"/"),filename:o.basename(n),extension:o.extname(n),sizeBytes:t.size,modified:t.mtime.toISOString()}};return i&&(a.frontmatter=i),r&&(a.json=r),a}function he(e){const t=function(){const e=new y({allErrors:!0});return w(e),e.addKeyword({keyword:"glob",type:"string",schemaType:"string",validate:(e,t)=>a.isMatch(t,e)}),e}();return e.map(((e,i)=>({rule:e,validate:t.compile({$id:`rule-${String(i)}`,...e.match})})))}function ue(e,t){return"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,((e,r)=>{const n=i.get(t,r);return null==n?"":"string"==typeof n?n:JSON.stringify(n)}))}function ge(e,t){const i={};for(const[r,n]of Object.entries(e))i[r]=ue(n,t);return i}async function fe(e,t,r,n){const o={split:(e,t)=>e.split(t),slice:(e,t,i)=>e.slice(t,i),join:(e,t)=>e.join(t),toLowerCase:e=>e.toLowerCase(),replace:(e,t,i)=>e.replace(t,i),get:(e,t)=>i.get(e,t)};let a={};const s=n??console;for(const{rule:i,validate:n}of e)if(n(t)){const e=ge(i.set,t);if(a={...a,...e},i.map){let e;if("string"==typeof i.map){if(e=r?.[i.map],!e){s.warn(`Map reference "${i.map}" not found in named maps. Skipping map transformation.`);continue}}else e=i.map;try{const i=new d.JsonMap(e,o),r=await i.transform(t);r&&"object"==typeof r&&!Array.isArray(r)?a={...a,...r}:s.warn("JsonMap transformation did not return an object; skipping merge.")}catch(e){s.warn(`JsonMap transformation failed: ${e instanceof Error?e.message:String(e)}`)}}}return a}async function me(e,t,i,r,a){const s=o.extname(e),c=await n.stat(e),l=await le(e,s),d=de(e,c,l.frontmatter,l.json),h=await fe(t,d,r,a),u=await x(e,i);return{inferred:h,enrichment:u,metadata:{...h,...u??{}},attributes:d,extracted:l}}function pe(e,t){const i=[];for(let r=0;r<t;r++)i.push(ie(e,r));return i}function ye(e,t=1){if(!e)return t;const i=e.total_chunks;return"number"==typeof i?i:t}class we{config;embeddingProvider;vectorStore;compiledRules;logger;constructor(e,t,i,r,n){this.config=e,this.embeddingProvider=t,this.vectorStore=i,this.compiledRules=r,this.logger=n}async processFile(e){try{const t=o.extname(e),{metadata:i,extracted:r}=await me(e,this.compiledRules,this.config.metadataDir,this.config.maps,this.logger);if(!r.text.trim())return void this.logger.debug({filePath:e},"Skipping empty file");const n=X(r.text),a=ie(e,0),s=await this.vectorStore.getPayload(a);if(s&&s.content_hash===n)return void this.logger.debug({filePath:e},"Content unchanged, skipping");const c=ye(s),l=this.config.chunkSize??1e3,d=function(e,t,i){const r=e.toLowerCase();return".md"===r||".markdown"===r?new b.MarkdownTextSplitter({chunkSize:t,chunkOverlap:i}):new b.RecursiveCharacterTextSplitter({chunkSize:t,chunkOverlap:i})}(t,l,this.config.chunkOverlap??200),h=await d.splitText(r.text),u=await this.embeddingProvider.embed(h),g=h.map(((t,r)=>({id:ie(e,r),vector:u[r],payload:{...i,file_path:e.replace(/\\/g,"/"),chunk_index:r,total_chunks:h.length,content_hash:n,chunk_text:t}})));if(await this.vectorStore.upsert(g),c>h.length){const t=pe(e,c).slice(h.length);await this.vectorStore.delete(t)}this.logger.info({filePath:e,chunks:h.length},"File processed successfully")}catch(t){this.logger.error({filePath:e,error:t},"Failed to process file")}}async deleteFile(e){try{const t=ie(e,0),i=await this.vectorStore.getPayload(t),r=pe(e,ye(i));await this.vectorStore.delete(r),await z(e,this.config.metadataDir),this.logger.info({filePath:e},"File deleted from index")}catch(t){this.logger.error({filePath:e,error:t},"Failed to delete file")}}async processMetadataUpdate(e,t){try{const i={...await x(e,this.config.metadataDir)??{},...t};await S(e,this.config.metadataDir,i);const r=ie(e,0),n=await this.vectorStore.getPayload(r);if(!n)return null;const o=ye(n),a=pe(e,o);return await this.vectorStore.setPayload(a,i),this.logger.info({filePath:e,chunks:o},"Metadata updated"),i}catch(t){return this.logger.error({filePath:e,error:t},"Failed to update metadata"),null}}async processRulesUpdate(e){try{const t=ie(e,0),i=await this.vectorStore.getPayload(t);if(!i)return this.logger.debug({filePath:e},"File not indexed, skipping"),null;const{metadata:r}=await me(e,this.compiledRules,this.config.metadataDir,this.config.maps,this.logger),n=ye(i),o=pe(e,n);return await this.vectorStore.setPayload(o,r),this.logger.info({filePath:e,chunks:n},"Rules re-applied"),r}catch(t){return this.logger.error({filePath:e,error:t},"Failed to re-apply rules"),null}}updateRules(e){this.compiledRules=e,this.logger.info({rules:e.length},"Inference rules updated")}}class be{debounceMs;concurrency;rateLimitPerMinute;started=!1;active=0;debounceTimers=new Map;latestByKey=new Map;normalQueue=[];lowQueue=[];tokens;lastRefillMs=Date.now();drainWaiters=[];constructor(e){this.debounceMs=e.debounceMs,this.concurrency=e.concurrency,this.rateLimitPerMinute=e.rateLimitPerMinute,this.tokens=this.rateLimitPerMinute??Number.POSITIVE_INFINITY}enqueue(e,t){const i=`${e.priority}:${e.path}`;this.latestByKey.set(i,{event:e,fn:t});const r=this.debounceTimers.get(i);r&&clearTimeout(r);const n=setTimeout((()=>{this.debounceTimers.delete(i);const e=this.latestByKey.get(i);e&&(this.latestByKey.delete(i),this.push(e),this.pump())}),this.debounceMs);this.debounceTimers.set(i,n)}process(){this.started=!0,this.pump()}async drain(){this.isIdle()||await new Promise((e=>{this.drainWaiters.push(e)}))}push(e){"low"===e.event.priority?this.lowQueue.push(e):this.normalQueue.push(e)}refillTokens(e){if(void 0===this.rateLimitPerMinute)return;const t=Math.max(0,e-this.lastRefillMs)*(this.rateLimitPerMinute/6e4);this.tokens=Math.min(this.rateLimitPerMinute,this.tokens+t),this.lastRefillMs=e}takeToken(){const e=Date.now();return this.refillTokens(e),!(this.tokens<1)&&(this.tokens-=1,!0)}nextItem(){return this.normalQueue.shift()??this.lowQueue.shift()}pump(){if(this.started){for(;this.active<this.concurrency;){const e=this.nextItem();if(!e)break;if(!this.takeToken()){"low"===e.event.priority?this.lowQueue.unshift(e):this.normalQueue.unshift(e),setTimeout((()=>{this.pump()}),250);break}this.active+=1,Promise.resolve().then((()=>e.fn(e.event))).finally((()=>{this.active-=1,this.pump(),this.maybeResolveDrain()}))}this.maybeResolveDrain()}}isIdle(){return 0===this.active&&0===this.normalQueue.length&&0===this.lowQueue.length&&0===this.debounceTimers.size&&0===this.latestByKey.size}maybeResolveDrain(){if(!this.isIdle())return;const e=this.drainWaiters;this.drainWaiters=[];for(const t of e)t()}}class ve{client;collectionName;dims;logger;constructor(e,t,i){this.client=new v.QdrantClient({url:e.url,apiKey:e.apiKey,checkCompatibility:!1}),this.collectionName=e.collectionName,this.dims=t,this.logger=i}async ensureCollection(){try{const e=await this.client.getCollections();e.collections.some((e=>e.name===this.collectionName))||await this.client.createCollection(this.collectionName,{vectors:{size:this.dims,distance:"Cosine"}})}catch(e){throw new Error(`Failed to ensure collection "${this.collectionName}": ${String(e)}`)}}async upsert(e){0!==e.length&&await V((async t=>{if(t>1){const i={attempt:t,operation:"qdrant.upsert",points:e.length};this.logger?this.logger.warn(i,"Retrying Qdrant upsert"):console.warn(i,"Retrying Qdrant upsert")}await this.client.upsert(this.collectionName,{wait:!0,points:e.map((e=>({id:e.id,vector:e.vector,payload:e.payload})))})}),{attempts:5,baseDelayMs:500,maxDelayMs:1e4,jitter:.2,onRetry:({attempt:e,delayMs:t,error:i})=>{const r={attempt:e,delayMs:t,operation:"qdrant.upsert",error:i};this.logger?this.logger.warn(r,"Qdrant upsert failed; will retry"):console.warn(r,"Qdrant upsert failed; will retry")}})}async delete(e){0!==e.length&&await V((async t=>{if(t>1){const i={attempt:t,operation:"qdrant.delete",ids:e.length};this.logger?this.logger.warn(i,"Retrying Qdrant delete"):console.warn(i,"Retrying Qdrant delete")}await this.client.delete(this.collectionName,{wait:!0,points:e})}),{attempts:5,baseDelayMs:500,maxDelayMs:1e4,jitter:.2,onRetry:({attempt:e,delayMs:t,error:i})=>{const r={attempt:e,delayMs:t,operation:"qdrant.delete",error:i};this.logger?this.logger.warn(r,"Qdrant delete failed; will retry"):console.warn(r,"Qdrant delete failed; will retry")}})}async setPayload(e,t){0!==e.length&&await this.client.setPayload(this.collectionName,{wait:!0,points:e,payload:t})}async getPayload(e){try{const t=await this.client.retrieve(this.collectionName,{ids:[e],with_payload:!0,with_vector:!1});return 0===t.length?null:t[0].payload}catch{return null}}async search(e,t,i){return(await this.client.search(this.collectionName,{vector:e,limit:t,with_payload:!0,...i?{filter:i}:{}})).map((e=>({id:String(e.id),score:e.score,payload:e.payload})))}async*scroll(e,t=100){let i;for(;;){const r=await this.client.scroll(this.collectionName,{limit:t,with_payload:!0,with_vector:!1,...e?{filter:e}:{},...void 0!==i?{offset:i}:{}});for(const e of r.points)yield{id:String(e.id),payload:e.payload};const n=r.next_page_offset;if(null==n)break;if("string"!=typeof n&&"number"!=typeof n)break;i=n}}}class Me{config;queue;processor;logger;watcher;constructor(e,t,i,r){this.config=e,this.queue=t,this.processor=i,this.logger=r}start(){this.watcher=s.watch(this.config.paths,{ignored:this.config.ignored,usePolling:this.config.usePolling,interval:this.config.pollIntervalMs,awaitWriteFinish:!!this.config.stabilityThresholdMs&&{stabilityThreshold:this.config.stabilityThresholdMs},ignoreInitial:!1}),this.watcher.on("add",(e=>{this.logger.debug({path:e},"File added"),this.queue.enqueue({type:"create",path:e,priority:"normal"},(()=>this.processor.processFile(e)))})),this.watcher.on("change",(e=>{this.logger.debug({path:e},"File changed"),this.queue.enqueue({type:"modify",path:e,priority:"normal"},(()=>this.processor.processFile(e)))})),this.watcher.on("unlink",(e=>{this.logger.debug({path:e},"File removed"),this.queue.enqueue({type:"delete",path:e,priority:"normal"},(()=>this.processor.deleteFile(e)))})),this.watcher.on("error",(e=>{this.logger.error({error:e},"Watcher error")})),this.queue.process(),this.logger.info({paths:this.config.paths},"Filesystem watcher started")}async stop(){this.watcher&&(await this.watcher.close(),this.watcher=void 0,this.logger.info("Filesystem watcher stopped"))}}class Pe{config;configPath;logger;watcher;queue;server;processor;configWatcher;configDebounce;constructor(e,t){this.config=e,this.configPath=t}async start(){const e=Y(this.config.logging);let t;this.logger=e;try{t=Z(this.config.embedding,e)}catch(t){throw e.fatal({error:t},"Failed to create embedding provider"),t}const i=new ve(this.config.vectorStore,t.dimensions,e);await i.ensureCollection();const r=he(this.config.inferenceRules??[]),n={metadataDir:this.config.metadataDir??".jeeves-metadata",chunkSize:this.config.embedding.chunkSize,chunkOverlap:this.config.embedding.chunkOverlap,maps:this.config.maps},o=new we(n,t,i,r,e);this.processor=o;const a=new be({debounceMs:this.config.watch.debounceMs??2e3,concurrency:this.config.embedding.concurrency??5,rateLimitPerMinute:this.config.embedding.rateLimitPerMinute});this.queue=a;const s=new Me(this.config.watch,a,o,e);this.watcher=s;const c=F({processor:o,vectorStore:i,embeddingProvider:t,queue:a,config:this.config,logger:e});this.server=c,await c.listen({host:this.config.api?.host??"127.0.0.1",port:this.config.api?.port??3456}),s.start(),this.startConfigWatch(),e.info("jeeves-watcher started")}async stop(){if(await this.stopConfigWatch(),this.watcher&&await this.watcher.stop(),this.queue){const e=this.config.shutdownTimeoutMs??1e4;await Promise.race([this.queue.drain().then((()=>!0)),new Promise((t=>{setTimeout((()=>{t(!1)}),e)}))])||this.logger?.warn({timeoutMs:e},"Queue drain timeout hit, forcing shutdown")}this.server&&await this.server.close(),this.logger?.info("jeeves-watcher stopped")}startConfigWatch(){const e=this.logger;if(!e)return;if(!(this.config.configWatch?.enabled??!0))return;if(!this.configPath)return void e.debug("Config watch enabled, but no config path was provided");const t=this.config.configWatch?.debounceMs??1e4;this.configWatcher=s.watch(this.configPath,{ignoreInitial:!0}),this.configWatcher.on("change",(()=>{this.configDebounce&&clearTimeout(this.configDebounce),this.configDebounce=setTimeout((()=>{this.reloadConfig()}),t)})),this.configWatcher.on("error",(t=>{e.error({error:t},"Config watcher error")})),e.info({configPath:this.configPath,debounceMs:t},"Config watcher started")}async stopConfigWatch(){this.configDebounce&&(clearTimeout(this.configDebounce),this.configDebounce=void 0),this.configWatcher&&(await this.configWatcher.close(),this.configWatcher=void 0)}async reloadConfig(){const e=this.logger,t=this.processor;if(e&&t&&this.configPath){e.info({configPath:this.configPath},"Config change detected, reloading...");try{const i=await G(this.configPath);this.config=i;const r=he(i.inferenceRules??[]);t.updateRules(r),e.info({configPath:this.configPath,rules:r.length},"Config reloaded")}catch(t){e.error({error:t},"Failed to reload config")}}}}e.DocumentProcessor=we,e.EventQueue=be,e.FileSystemWatcher=Me,e.JeevesWatcher=Pe,e.VectorStoreClient=ve,e.apiConfigSchema=L,e.applyRules=fe,e.buildAttributes=de,e.compileRules=he,e.configWatchConfigSchema=_,e.contentHash=X,e.createApiServer=F,e.createEmbeddingProvider=Z,e.createLogger=Y,e.deleteMetadata=z,e.embeddingConfigSchema=A,e.extractText=le,e.inferenceRuleSchema=$,e.jeevesWatcherConfigSchema=K,e.loadConfig=G,e.loggingConfigSchema=O,e.metadataPath=k,e.pointId=ie,e.readMetadata=x,e.startFromConfig=async function(e){const t=await G(e),i=new Pe(t,e),r=async()=>{await i.stop(),process.exit(0)};return process.on("SIGTERM",(()=>{r()})),process.on("SIGINT",(()=>{r()})),await i.start(),i},e.vectorStoreConfigSchema=Q,e.watchConfigSchema=W,e.writeMetadata=S}(this["jeeves-watcher"]=this["jeeves-watcher"]||{},Fastify,radash,node_crypto,promises,node_path,picomatch,chokidar,cosmiconfig,zod,jsonmap,googleGenai,pino,uuid,cheerio,yaml,mammoth,Ajv,addFormats,textsplitters,jsClientRest);
|
|
1
|
+
!function(e,t,i,r,o,n,s,a,c,l,d,h,u,g,f,p,m,y,w,b,v){"use strict";function M(e){var t=Object.create(null);return e&&Object.keys(e).forEach((function(i){if("default"!==i){var r=Object.getOwnPropertyDescriptor(e,i);Object.defineProperty(t,i,r.get?r:{enumerable:!0,get:function(){return e[i]}})}})),t.default=e,Object.freeze(t)}var P=M(g);function S(e){if(e instanceof Error)return e;if("string"==typeof e)return new Error(e);const t=String("object"==typeof e&&null!==e&&"message"in e?e.message:e),i=new Error(t);return i.cause=e,i}function k(e){const t=e.replace(/\\/g,"/"),i=t.search(/[*?\[]/);if(-1===i)return r.resolve(e);const o=t.slice(0,i),n=o.endsWith("/")?o.slice(0,-1):r.dirname(o);return r.resolve(n)}async function*x(e){let t;try{t=(await i.readdir(e,{withFileTypes:!0})).map((e=>({name:e.name,isDirectory:e.isDirectory()})))}catch{return}for(const o of t){const t=r.resolve(e,o.name);if(o.isDirectory)yield*x(t);else try{(await i.stat(t)).isFile()&&(yield t)}catch{}}}async function z(e,t,i,r){const n=await async function(e,t=[]){const i=e.map((e=>e.replace(/\\/g,"/"))),r=t.map((e=>e.replace(/\\/g,"/"))),n=o(i,{dot:!0}),s=r.length?o(r,{dot:!0}):()=>!1,a=Array.from(new Set(e.map(k))),c=new Set;for(const e of a)for await(const t of x(e)){const e=t.replace(/\\/g,"/");s(e)||n(e)&&c.add(t)}return Array.from(c)}(e,t);for(const e of n)await i[r](e);return n.length}function j(e,t=!1){let i=e.replace(/\\/g,"/").toLowerCase();return t&&(i=i.replace(/^([a-z]):/,((e,t)=>t))),i}function C(e,t){const i=j(e,!0),o=s.createHash("sha256").update(i,"utf8").digest("hex");return r.join(t,`${o}.meta.json`)}async function F(e,t){try{const r=await i.readFile(C(e,t),"utf8");return JSON.parse(r)}catch{return null}}async function R(e,t,o){const n=C(e,t);await i.mkdir(r.dirname(n),{recursive:!0}),await i.writeFile(n,JSON.stringify(o,null,2),"utf8")}async function T(e,t){try{await i.rm(C(e,t))}catch{}}const E=["file_path","chunk_index","total_chunks","content_hash","chunk_text"];function I(e){const{processor:i,vectorStore:r,embeddingProvider:o,logger:s,config:a}=e,c=t({logger:!1});var l;return c.get("/status",(()=>({status:"ok",uptime:process.uptime()}))),c.post("/metadata",(l={processor:i,logger:s},async(e,t)=>{try{const{path:t,metadata:i}=e.body;return await l.processor.processMetadataUpdate(t,i),{ok:!0}}catch(e){return l.logger.error({err:S(e)},"Metadata update failed"),t.status(500).send({error:"Internal server error"})}})),c.post("/search",function(e){return async(t,i)=>{try{const{query:i,limit:r=10}=t.body,o=await e.embeddingProvider.embed([i]);return await e.vectorStore.search(o[0],r)}catch(t){return e.logger.error({err:S(t)},"Search failed"),i.status(500).send({error:"Internal server error"})}}}({embeddingProvider:o,vectorStore:r,logger:s})),c.post("/reindex",function(e){return async(t,i)=>{try{const t=await z(e.config.watch.paths,e.config.watch.ignored,e.processor,"processFile");return await i.status(200).send({ok:!0,filesIndexed:t})}catch(t){return e.logger.error({err:S(t)},"Reindex failed"),await i.status(500).send({error:"Internal server error"})}}}({config:a,processor:i,logger:s})),c.post("/rebuild-metadata",function(e){return async(t,i)=>{try{const t=e.config.metadataDir??".jeeves-metadata",r=[...E];for await(const i of e.vectorStore.scroll()){const e=i.payload,o=e.file_path;if("string"!=typeof o||0===o.length)continue;const s=n.omit(e,r);await R(o,t,s)}return await i.status(200).send({ok:!0})}catch(t){return e.logger.error({err:S(t)},"Rebuild metadata failed"),await i.status(500).send({error:"Internal server error"})}}}({config:a,vectorStore:r,logger:s})),c.post("/config-reindex",function(e){return async(t,i)=>{try{const r=t.body.scope??"rules";return(async()=>{try{if("rules"===r){const t=await z(e.config.watch.paths,e.config.watch.ignored,e.processor,"processRulesUpdate");e.logger.info({scope:r,filesProcessed:t},"Config reindex (rules) completed")}else{const t=await z(e.config.watch.paths,e.config.watch.ignored,e.processor,"processFile");e.logger.info({scope:r,filesProcessed:t},"Config reindex (full) completed")}}catch(t){e.logger.error({err:S(t),scope:r},"Config reindex failed")}})(),await i.status(200).send({status:"started",scope:r})}catch(t){return e.logger.error({err:S(t)},"Config reindex request failed"),await i.status(500).send({error:"Internal server error"})}}}({config:a,processor:i,logger:s})),c}const D={metadataDir:".jeeves-watcher",shutdownTimeoutMs:1e4},A={enabled:!0,debounceMs:1e3},W={host:"127.0.0.1",port:3456},N={level:"info"},q={debounceMs:300,stabilityThresholdMs:500,usePolling:!1,pollIntervalMs:1e3},_={chunkSize:1e3,chunkOverlap:200,dimensions:3072,rateLimitPerMinute:300,concurrency:5},L=c.z.object({paths:c.z.array(c.z.string()).min(1).describe('Glob patterns for files to watch (e.g., "**/*.md"). At least one required.'),ignored:c.z.array(c.z.string()).optional().describe('Glob patterns to exclude from watching (e.g., "**/node_modules/**").'),pollIntervalMs:c.z.number().optional().describe("Polling interval in milliseconds when usePolling is enabled."),usePolling:c.z.boolean().optional().describe("Use polling instead of native file system events (for network drives)."),debounceMs:c.z.number().optional().describe("Debounce delay in milliseconds for file change events."),stabilityThresholdMs:c.z.number().optional().describe("Time in milliseconds a file must remain unchanged before processing.")}),O=c.z.object({enabled:c.z.boolean().optional().describe("Enable automatic reloading when config file changes."),debounceMs:c.z.number().optional().describe("Debounce delay in milliseconds for config file change detection.")}),$=c.z.object({provider:c.z.string().default("gemini").describe('Embedding provider name (e.g., "gemini", "openai").'),model:c.z.string().default("gemini-embedding-001").describe('Embedding model identifier (e.g., "gemini-embedding-001", "text-embedding-3-small").'),chunkSize:c.z.number().optional().describe("Maximum chunk size in characters for text splitting."),chunkOverlap:c.z.number().optional().describe("Character overlap between consecutive chunks."),dimensions:c.z.number().optional().describe("Embedding vector dimensions (must match model output)."),apiKey:c.z.string().optional().describe("API key for embedding provider (supports ${ENV_VAR} substitution)."),rateLimitPerMinute:c.z.number().optional().describe("Maximum embedding API requests per minute (rate limiting)."),concurrency:c.z.number().optional().describe("Maximum concurrent embedding requests.")}),Q=c.z.object({url:c.z.string().describe('Qdrant server URL (e.g., "http://localhost:6333").'),collectionName:c.z.string().describe("Qdrant collection name for vector storage."),apiKey:c.z.string().optional().describe("Qdrant API key for authentication (supports ${ENV_VAR} substitution).")}),K=c.z.object({host:c.z.string().optional().describe('Host address for API server (e.g., "127.0.0.1", "0.0.0.0").'),port:c.z.number().optional().describe("Port for API server (e.g., 3456).")}),J=c.z.object({level:c.z.string().optional().describe("Logging level (trace, debug, info, warn, error, fatal)."),file:c.z.string().optional().describe("Path to log file (logs to stdout if omitted).")}),G=c.z.object({match:c.z.record(c.z.string(),c.z.unknown()).describe("JSON Schema object to match against file attributes."),set:c.z.record(c.z.string(),c.z.unknown()).describe("Metadata fields to set when match succeeds."),map:c.z.union([l.jsonMapMapSchema,c.z.string()]).optional().describe("JsonMap transformation (inline definition or named map reference).")}),U=c.z.object({watch:L.describe("File system watch configuration."),configWatch:O.optional().describe("Configuration file watch settings."),embedding:$.describe("Embedding model configuration."),vectorStore:Q.describe("Qdrant vector store configuration."),metadataDir:c.z.string().optional().describe("Directory for persisted metadata sidecar files."),api:K.optional().describe("API server configuration."),extractors:c.z.record(c.z.string(),c.z.unknown()).optional().describe("Extractor configurations keyed by name."),inferenceRules:c.z.array(G).optional().describe("Rules for inferring metadata from file attributes."),maps:c.z.record(c.z.string(),l.jsonMapMapSchema).optional().describe("Reusable named JsonMap transformations."),logging:J.optional().describe("Logging configuration."),shutdownTimeoutMs:c.z.number().optional().describe("Timeout in milliseconds for graceful shutdown.")}),V=/\$\{([^}]+)\}/g;function B(e){if("string"==typeof e)return function(e){return e.replace(V,((e,t)=>{const i=process.env[t];if(void 0===i)throw new Error(`Environment variable \${${t}} referenced in config is not set.`);return i}))}(e);if(Array.isArray(e))return e.map((e=>B(e)));if(null!==e&&"object"==typeof e){const t={};for(const[i,r]of Object.entries(e))t[i]=B(r);return t}return e}const H="jeeves-watcher";async function Y(e){const t=a.cosmiconfig(H),i=e?await t.load(e):await t.search();if(!i||i.isEmpty)throw new Error("No jeeves-watcher configuration found. Create a .jeeves-watcherrc or jeeves-watcher.config.{js,ts,json,yaml} file.");try{const e=U.parse(i.config);return B((r=e,{...D,...r,watch:{...q,...r.watch},configWatch:{...A,...r.configWatch},embedding:{..._,...r.embedding},api:{...W,...r.api},logging:{...N,...r.logging}}))}catch(e){if(e instanceof c.ZodError){const t=e.issues.map((e=>`${e.path.join(".")}: ${e.message}`)).join("; ");throw new Error(`Invalid jeeves-watcher configuration: ${t}`)}throw e}var r}function Z(e){return e||{warn(e,t){t?console.warn(e,t):console.warn(e)}}}function X(e,t){return e<=0?Promise.resolve():new Promise(((i,r)=>{const o=setTimeout((()=>{s(),i()}),e),n=()=>{s(),r(new Error("Retry sleep aborted"))},s=()=>{clearTimeout(o),t&&t.removeEventListener("abort",n)};if(t){if(t.aborted)return void n();t.addEventListener("abort",n,{once:!0})}}))}function ee(e,t,i,r=0){const o=Math.max(0,e-1),n=Math.min(i,t*2**o),s=r>0?1+Math.random()*r:1;return Math.round(n*s)}async function te(e,t){const i=Math.max(1,t.attempts);let r;for(let o=1;o<=i;o++)try{return await e(o)}catch(e){r=e;if(o>=i)break;const n=ee(o,t.baseDelayMs,t.maxDelayMs,t.jitter);t.onRetry?.({attempt:o,attempts:i,delayMs:n,error:e}),await X(n,t.signal)}throw r}const ie=new Map([["mock",function(e){return function(e){return{dimensions:e,embed:t=>Promise.resolve(t.map((t=>{const i=s.createHash("sha256").update(t,"utf8").digest(),r=[];for(let t=0;t<e;t++){const e=i[t%i.length];r.push(e/127.5-1)}return r})))}}(e.dimensions??768)}],["gemini",function(e,t){if(!e.apiKey)throw new Error("Gemini embedding provider requires config.embedding.apiKey");const i=e.dimensions??3072,r=Z(t),o=new d.GoogleGenerativeAIEmbeddings({apiKey:e.apiKey,model:e.model});return{dimensions:i,async embed(t){const n=await te((async i=>(i>1&&r.warn({attempt:i,provider:"gemini",model:e.model},"Retrying embedding request"),o.embedDocuments(t))),{attempts:5,baseDelayMs:500,maxDelayMs:1e4,jitter:.2,onRetry:({attempt:t,delayMs:i,error:o})=>{r.warn({attempt:t,delayMs:i,provider:"gemini",model:e.model,err:S(o)},"Embedding call failed; will retry")}});for(const e of n)if(e.length!==i)throw new Error(`Gemini embedding returned invalid dimensions: expected ${String(i)}, got ${String(e.length)}`);return n}}}]]);function re(e,t){const i=ie.get(e.provider);if(!i)throw new Error(`Unsupported embedding provider: ${e.provider}`);return i(e,t)}function oe(e){const t=e?.level??"info";if(e?.file){const i=h.transport({target:"pino/file",options:{destination:e.file,mkdir:!0}});return h({level:t},i)}return h({level:t})}function ne(e){return s.createHash("sha256").update(e,"utf8").digest("hex")}const se="6a6f686e-6761-4c74-ad6a-656576657321";function ae(e,t){const i=void 0!==t?`${j(e)}#${String(t)}`:j(e);return u.v5(i,se)}const ce=["content","body","text","snippet","subject","description","summary","transcript"];function le(e){if(!e||"object"!=typeof e)return JSON.stringify(e);const t=e;for(const e of ce){const i=t[e];if("string"==typeof i&&i.trim())return i}return JSON.stringify(e)}async function de(e){const t=await i.readFile(e,"utf8"),{frontmatter:r,body:o}=function(e){const t=e.replace(/^\uFEFF/,"");if(!/^\s*---/.test(t))return{body:e};const i=/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/m.exec(t);if(!i)return{body:e};const[,r,o]=i,n=f.load(r);return{frontmatter:n&&"object"==typeof n&&!Array.isArray(n)?n:void 0,body:o}}(t);return{text:o,frontmatter:r}}async function he(e){return{text:await i.readFile(e,"utf8")}}async function ue(e){const t=await i.readFile(e,"utf8"),r=P.load(t);r("script, style").remove();return{text:r("body").text().trim()||r.text().trim()}}const ge=new Map([[".md",de],[".markdown",de],[".txt",he],[".text",he],[".json",async function(e){const t=await i.readFile(e,"utf8"),r=JSON.parse(t),o=r&&"object"==typeof r&&!Array.isArray(r)?r:void 0;return{text:le(r),json:o}}],[".pdf",async function(e){const t=await i.readFile(e),r=new Uint8Array(t),{extractText:o}=await import("unpdf"),{text:n}=await o(r);return{text:Array.isArray(n)?n.join("\n\n"):n}}],[".docx",async function(e){const t=await i.readFile(e);return{text:(await p.extractRawText({buffer:t})).value}}],[".html",ue],[".htm",ue]]);async function fe(e,t){const i=ge.get(t.toLowerCase());return i?i(e):he(e)}function pe(e,t){return"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,((e,i)=>{const r=n.get(t,i);return null==r?"":"string"==typeof r?r:JSON.stringify(r)}))}function me(e,t){const i={};for(const[r,o]of Object.entries(e))i[r]=pe(o,t);return i}async function ye(e,t,i,r){const o={split:(e,t)=>e.split(t),slice:(e,t,i)=>e.slice(t,i),join:(e,t)=>e.join(t),toLowerCase:e=>e.toLowerCase(),replace:(e,t,i)=>e.replace(t,i),get:(e,t)=>n.get(e,t)};let s={};const a=r??console;for(const{rule:r,validate:n}of e)if(n(t)){const e=me(r.set,t);if(s={...s,...e},r.map){let e;if("string"==typeof r.map){if(e=i?.[r.map],!e){a.warn(`Map reference "${r.map}" not found in named maps. Skipping map transformation.`);continue}}else e=r.map;try{const i=new l.JsonMap(e,o),r=await i.transform(t);r&&"object"==typeof r&&!Array.isArray(r)?s={...s,...r}:a.warn("JsonMap transformation did not return an object; skipping merge.")}catch(e){a.warn(`JsonMap transformation failed: ${e instanceof Error?e.message:String(e)}`)}}}return s}function we(e,t,i,o){const n=e.replace(/\\/g,"/"),s={file:{path:n,directory:r.dirname(n).replace(/\\/g,"/"),filename:r.basename(n),extension:r.extname(n),sizeBytes:t.size,modified:t.mtime.toISOString()}};return i&&(s.frontmatter=i),o&&(s.json=o),s}function be(e){const t=function(){const e=new m({allErrors:!0});return y(e),e.addKeyword({keyword:"glob",type:"string",schemaType:"string",validate:(e,t)=>o.isMatch(t,e)}),e}();return e.map(((e,i)=>({rule:e,validate:t.compile({$id:`rule-${String(i)}`,...e.match})})))}async function ve(e,t,o,n,s){const a=r.extname(e),c=await i.stat(e),l=await fe(e,a),d=we(e,c,l.frontmatter,l.json),h=await ye(t,d,n,s),u=await F(e,o);return{inferred:h,enrichment:u,metadata:{...h,...u??{}},attributes:d,extracted:l}}function Me(e,t){const i=[];for(let r=0;r<t;r++)i.push(ae(e,r));return i}function Pe(e,t=1){if(!e)return t;const i=e.total_chunks;return"number"==typeof i?i:t}class Se{config;embeddingProvider;vectorStore;compiledRules;logger;constructor(e,t,i,r,o){this.config=e,this.embeddingProvider=t,this.vectorStore=i,this.compiledRules=r,this.logger=o}async processFile(e){try{const t=r.extname(e),{metadata:i,extracted:o}=await ve(e,this.compiledRules,this.config.metadataDir,this.config.maps,this.logger);if(!o.text.trim())return void this.logger.debug({filePath:e},"Skipping empty file");const n=ne(o.text),s=ae(e,0),a=await this.vectorStore.getPayload(s);if(a&&a.content_hash===n)return void this.logger.debug({filePath:e},"Content unchanged, skipping");const c=Pe(a),l=this.config.chunkSize??1e3,d=function(e,t,i){const r=e.toLowerCase();return".md"===r||".markdown"===r?new w.MarkdownTextSplitter({chunkSize:t,chunkOverlap:i}):new w.RecursiveCharacterTextSplitter({chunkSize:t,chunkOverlap:i})}(t,l,this.config.chunkOverlap??200),h=await d.splitText(o.text),u=await this.embeddingProvider.embed(h),g=h.map(((t,r)=>({id:ae(e,r),vector:u[r],payload:{...i,file_path:e.replace(/\\/g,"/"),chunk_index:r,total_chunks:h.length,content_hash:n,chunk_text:t}})));if(await this.vectorStore.upsert(g),c>h.length){const t=Me(e,c).slice(h.length);await this.vectorStore.delete(t)}this.logger.info({filePath:e,chunks:h.length},"File processed successfully")}catch(t){this.logger.error({filePath:e,err:S(t)},"Failed to process file")}}async deleteFile(e){try{const t=ae(e,0),i=await this.vectorStore.getPayload(t),r=Me(e,Pe(i));await this.vectorStore.delete(r),await T(e,this.config.metadataDir),this.logger.info({filePath:e},"File deleted from index")}catch(t){this.logger.error({filePath:e,err:S(t)},"Failed to delete file")}}async processMetadataUpdate(e,t){try{const i={...await F(e,this.config.metadataDir)??{},...t};await R(e,this.config.metadataDir,i);const r=ae(e,0),o=await this.vectorStore.getPayload(r);if(!o)return null;const n=Pe(o),s=Me(e,n);return await this.vectorStore.setPayload(s,i),this.logger.info({filePath:e,chunks:n},"Metadata updated"),i}catch(t){return this.logger.error({filePath:e,err:S(t)},"Failed to update metadata"),null}}async processRulesUpdate(e){try{const t=ae(e,0),i=await this.vectorStore.getPayload(t);if(!i)return this.logger.debug({filePath:e},"File not indexed, skipping"),null;const{metadata:r}=await ve(e,this.compiledRules,this.config.metadataDir,this.config.maps,this.logger),o=Pe(i),n=Me(e,o);return await this.vectorStore.setPayload(n,r),this.logger.info({filePath:e,chunks:o},"Rules re-applied"),r}catch(t){return this.logger.error({filePath:e,err:S(t)},"Failed to re-apply rules"),null}}updateRules(e){this.compiledRules=e,this.logger.info({rules:e.length},"Inference rules updated")}}class ke{debounceMs;concurrency;rateLimitPerMinute;started=!1;active=0;debounceTimers=new Map;latestByKey=new Map;normalQueue=[];lowQueue=[];tokens;lastRefillMs=Date.now();drainWaiters=[];constructor(e){this.debounceMs=e.debounceMs,this.concurrency=e.concurrency,this.rateLimitPerMinute=e.rateLimitPerMinute,this.tokens=this.rateLimitPerMinute??Number.POSITIVE_INFINITY}enqueue(e,t){const i=`${e.priority}:${e.path}`;this.latestByKey.set(i,{event:e,fn:t});const r=this.debounceTimers.get(i);r&&clearTimeout(r);const o=setTimeout((()=>{this.debounceTimers.delete(i);const e=this.latestByKey.get(i);e&&(this.latestByKey.delete(i),this.push(e),this.pump())}),this.debounceMs);this.debounceTimers.set(i,o)}process(){this.started=!0,this.pump()}async drain(){this.isIdle()||await new Promise((e=>{this.drainWaiters.push(e)}))}push(e){"low"===e.event.priority?this.lowQueue.push(e):this.normalQueue.push(e)}refillTokens(e){if(void 0===this.rateLimitPerMinute)return;const t=Math.max(0,e-this.lastRefillMs)*(this.rateLimitPerMinute/6e4);this.tokens=Math.min(this.rateLimitPerMinute,this.tokens+t),this.lastRefillMs=e}takeToken(){const e=Date.now();return this.refillTokens(e),!(this.tokens<1)&&(this.tokens-=1,!0)}nextItem(){return this.normalQueue.shift()??this.lowQueue.shift()}pump(){if(this.started){for(;this.active<this.concurrency;){const e=this.nextItem();if(!e)break;if(!this.takeToken()){"low"===e.event.priority?this.lowQueue.unshift(e):this.normalQueue.unshift(e),setTimeout((()=>{this.pump()}),250);break}this.active+=1,Promise.resolve().then((()=>e.fn(e.event))).finally((()=>{this.active-=1,this.pump(),this.maybeResolveDrain()}))}this.maybeResolveDrain()}}isIdle(){return 0===this.active&&0===this.normalQueue.length&&0===this.lowQueue.length&&0===this.debounceTimers.size&&0===this.latestByKey.size}maybeResolveDrain(){if(!this.isIdle())return;const e=this.drainWaiters;this.drainWaiters=[];for(const t of e)t()}}class xe{client;collectionName;dims;log;constructor(e,t,i){this.client=new b.QdrantClient({url:e.url,apiKey:e.apiKey,checkCompatibility:!1}),this.collectionName=e.collectionName,this.dims=t,this.log=Z(i)}async ensureCollection(){try{const e=await this.client.getCollections();e.collections.some((e=>e.name===this.collectionName))||await this.client.createCollection(this.collectionName,{vectors:{size:this.dims,distance:"Cosine"}})}catch(e){throw new Error(`Failed to ensure collection "${this.collectionName}": ${String(e)}`)}}async upsert(e){0!==e.length&&await te((async t=>{t>1&&this.log.warn({attempt:t,operation:"qdrant.upsert",points:e.length},"Retrying Qdrant upsert"),await this.client.upsert(this.collectionName,{wait:!0,points:e.map((e=>({id:e.id,vector:e.vector,payload:e.payload})))})}),{attempts:5,baseDelayMs:500,maxDelayMs:1e4,jitter:.2,onRetry:({attempt:e,delayMs:t,error:i})=>{this.log.warn({attempt:e,delayMs:t,operation:"qdrant.upsert",err:S(i)},"Qdrant upsert failed; will retry")}})}async delete(e){0!==e.length&&await te((async t=>{t>1&&this.log.warn({attempt:t,operation:"qdrant.delete",ids:e.length},"Retrying Qdrant delete"),await this.client.delete(this.collectionName,{wait:!0,points:e})}),{attempts:5,baseDelayMs:500,maxDelayMs:1e4,jitter:.2,onRetry:({attempt:e,delayMs:t,error:i})=>{this.log.warn({attempt:e,delayMs:t,operation:"qdrant.delete",err:S(i)},"Qdrant delete failed; will retry")}})}async setPayload(e,t){0!==e.length&&await this.client.setPayload(this.collectionName,{wait:!0,points:e,payload:t})}async getPayload(e){try{const t=await this.client.retrieve(this.collectionName,{ids:[e],with_payload:!0,with_vector:!1});return 0===t.length?null:t[0].payload}catch{return null}}async search(e,t,i){return(await this.client.search(this.collectionName,{vector:e,limit:t,with_payload:!0,...i?{filter:i}:{}})).map((e=>({id:String(e.id),score:e.score,payload:e.payload})))}async*scroll(e,t=100){let i;for(;;){const r=await this.client.scroll(this.collectionName,{limit:t,with_payload:!0,with_vector:!1,...e?{filter:e}:{},...void 0!==i?{offset:i}:{}});for(const e of r.points)yield{id:String(e.id),payload:e.payload};const o=r.next_page_offset;if(null==o)break;if("string"!=typeof o&&"number"!=typeof o)break;i=o}}}class ze{config;queue;processor;logger;watcher;constructor(e,t,i,r){this.config=e,this.queue=t,this.processor=i,this.logger=r}start(){this.watcher=v.watch(this.config.paths,{ignored:this.config.ignored,usePolling:this.config.usePolling,interval:this.config.pollIntervalMs,awaitWriteFinish:!!this.config.stabilityThresholdMs&&{stabilityThreshold:this.config.stabilityThresholdMs},ignoreInitial:!1}),this.watcher.on("add",(e=>{this.logger.debug({path:e},"File added"),this.queue.enqueue({type:"create",path:e,priority:"normal"},(()=>this.processor.processFile(e)))})),this.watcher.on("change",(e=>{this.logger.debug({path:e},"File changed"),this.queue.enqueue({type:"modify",path:e,priority:"normal"},(()=>this.processor.processFile(e)))})),this.watcher.on("unlink",(e=>{this.logger.debug({path:e},"File removed"),this.queue.enqueue({type:"delete",path:e,priority:"normal"},(()=>this.processor.deleteFile(e)))})),this.watcher.on("error",(e=>{this.logger.error({err:S(e)},"Watcher error")})),this.queue.process(),this.logger.info({paths:this.config.paths},"Filesystem watcher started")}async stop(){this.watcher&&(await this.watcher.close(),this.watcher=void 0,this.logger.info("Filesystem watcher stopped"))}}class je{options;watcher;debounce;constructor(e){this.options=e}start(){this.options.enabled&&(this.watcher=v.watch(this.options.configPath,{ignoreInitial:!0}),this.watcher.on("change",(()=>{this.debounce&&clearTimeout(this.debounce),this.debounce=setTimeout((()=>{this.options.onChange()}),this.options.debounceMs)})),this.watcher.on("error",(e=>{this.options.logger.error({err:S(e)},"Config watcher error")})),this.options.logger.info({configPath:this.options.configPath,debounceMs:this.options.debounceMs},"Config watcher started"))}async stop(){this.debounce&&(clearTimeout(this.debounce),this.debounce=void 0),this.watcher&&(await this.watcher.close(),this.watcher=void 0)}}const Ce={loadConfig:Y,createLogger:oe,createEmbeddingProvider:re,createVectorStoreClient:(e,t,i)=>new xe(e,t,i),compileRules:be,createDocumentProcessor:(e,t,i,r,o)=>new Se(e,t,i,r,o),createEventQueue:e=>new ke(e),createFileSystemWatcher:(e,t,i,r)=>new ze(e,t,i,r),createApiServer:I};class Fe{config;configPath;factories;logger;watcher;queue;server;processor;configWatcher;constructor(e,t,i={}){this.config=e,this.configPath=t,this.factories={...Ce,...i}}async start(){const e=this.factories.createLogger(this.config.logging);let t;this.logger=e;try{t=this.factories.createEmbeddingProvider(this.config.embedding,e)}catch(t){throw e.fatal({err:S(t)},"Failed to create embedding provider"),t}const i=this.factories.createVectorStoreClient(this.config.vectorStore,t.dimensions,e);await i.ensureCollection();const r=this.factories.compileRules(this.config.inferenceRules??[]),o={metadataDir:this.config.metadataDir??".jeeves-metadata",chunkSize:this.config.embedding.chunkSize,chunkOverlap:this.config.embedding.chunkOverlap,maps:this.config.maps},n=this.factories.createDocumentProcessor(o,t,i,r,e);this.processor=n;const s=this.factories.createEventQueue({debounceMs:this.config.watch.debounceMs??2e3,concurrency:this.config.embedding.concurrency??5,rateLimitPerMinute:this.config.embedding.rateLimitPerMinute});this.queue=s;const a=this.factories.createFileSystemWatcher(this.config.watch,s,n,e);this.watcher=a;const c=this.factories.createApiServer({processor:n,vectorStore:i,embeddingProvider:t,queue:s,config:this.config,logger:e});this.server=c,await c.listen({host:this.config.api?.host??"127.0.0.1",port:this.config.api?.port??3456}),a.start(),this.startConfigWatch(),e.info("jeeves-watcher started")}async stop(){if(await this.stopConfigWatch(),this.watcher&&await this.watcher.stop(),this.queue){const e=this.config.shutdownTimeoutMs??1e4;await Promise.race([this.queue.drain().then((()=>!0)),new Promise((t=>{setTimeout((()=>{t(!1)}),e)}))])||this.logger?.warn({timeoutMs:e},"Queue drain timeout hit, forcing shutdown")}this.server&&await this.server.close(),this.logger?.info("jeeves-watcher stopped")}startConfigWatch(){const e=this.logger;if(!e)return;const t=this.config.configWatch?.enabled??!0;if(!t)return;if(!this.configPath)return void e.debug("Config watch enabled, but no config path was provided");const i=this.config.configWatch?.debounceMs??1e4;this.configWatcher=new je({configPath:this.configPath,enabled:t,debounceMs:i,logger:e,onChange:async()=>this.reloadConfig()}),this.configWatcher.start()}async stopConfigWatch(){this.configWatcher&&(await this.configWatcher.stop(),this.configWatcher=void 0)}async reloadConfig(){const e=this.logger,t=this.processor;if(e&&t&&this.configPath){e.info({configPath:this.configPath},"Config change detected, reloading...");try{const i=await this.factories.loadConfig(this.configPath);this.config=i;const r=this.factories.compileRules(i.inferenceRules??[]);t.updateRules(r),e.info({configPath:this.configPath,rules:r.length},"Config reloaded")}catch(t){e.error({err:S(t)},"Failed to reload config")}}}}e.DocumentProcessor=Se,e.EventQueue=ke,e.FileSystemWatcher=ze,e.JeevesWatcher=Fe,e.VectorStoreClient=xe,e.apiConfigSchema=K,e.applyRules=ye,e.buildAttributes=we,e.compileRules=be,e.configWatchConfigSchema=O,e.contentHash=ne,e.createApiServer=I,e.createEmbeddingProvider=re,e.createLogger=oe,e.deleteMetadata=T,e.embeddingConfigSchema=$,e.extractText=fe,e.inferenceRuleSchema=G,e.jeevesWatcherConfigSchema=U,e.loadConfig=Y,e.loggingConfigSchema=J,e.metadataPath=C,e.pointId=ae,e.readMetadata=F,e.startFromConfig=async function(e){const t=await Y(e),i=new Fe(t,e);return function(e){const t=async()=>{await e(),process.exit(0)};process.on("SIGTERM",(()=>{t()})),process.on("SIGINT",(()=>{t()}))}((()=>i.stop())),await i.start(),i},e.vectorStoreConfigSchema=Q,e.watchConfigSchema=L,e.writeMetadata=R}(this["jeeves-watcher"]=this["jeeves-watcher"]||{},Fastify,promises,node_path,picomatch,radash,node_crypto,cosmiconfig,zod,jsonmap,googleGenai,pino,uuid,cheerio,yaml,mammoth,Ajv,addFormats,textsplitters,jsClientRest,chokidar);
|