wiki-plugin-similarity 0.7.0 → 0.8.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.
@@ -1,4 +1,4 @@
1
- /* wiki-plugin-similarity - 0.7.0 - Mon, 20 Jul 2026 23:51:54 GMT */
1
+ /* wiki-plugin-similarity - 0.8.0 - Tue, 21 Jul 2026 08:09:09 GMT */
2
2
  (()=>{var q={high:.78,medium:.68,low:.58},M=q.medium,_=10,V=t=>{let e=[],n=[],o=[],f=null,b=null,d="search",y=!1,L=!1,E=null,k=null,w=(i,m)=>i===m||i.startsWith(m)&&/^[\s:]/.test(i.slice(m.length)),x=(i,m)=>i.slice(m.length).replace(/^\s*:?\s*/,"").trim();for(let i of t.split(`
3
3
  `)){let m=i.trim();if(!m||m.startsWith("#"))continue;let g=m.toUpperCase();if(w(g,"LIVE")){y=!0;continue}if(w(g,"AUTHOR")){!e.length&&d==="search"&&(d="author");continue}if(w(g,"REPORT")){d==="search"&&(d="report");continue}if(w(g,"KEYWORD")){d==="search"&&(d="keyword");continue}if(w(g,"BUILD")){d==="search"&&(d="build");continue}if(w(g,"FORCE")){L=!0;continue}if(w(g,"GHOST")){E=x(m,"GHOST"),d==="search"&&(d="ghost");continue}if(w(g,"BUTTON")){k=x(m,"BUTTON");continue}if(w(g,"ROSTER")){let S=x(m,"ROSTER");S&&n.push(S);continue}if(w(g,"FARM")){let S=x(m,"FARM");S&&o.push(S);continue}if(w(g,"LIST")){!e.length&&d==="search"&&(d="list");continue}if(w(g,"SIMILAR")){let S=x(g,"SIMILAR").toLowerCase();f=q[S]||M,!e.length&&d==="search"&&(d="similar");continue}if(w(g,"THRESHOLD")){let S=x(m,"THRESHOLD");f=q[S.toLowerCase()]??(parseFloat(S)||M);continue}if(w(g,"LIMIT")){b=parseInt(x(m,"LIMIT"))||_;continue}e.push(["PUBLIC","LOCAL","PRIVATE"].includes(g)?g:m)}return{mode:d,specs:e,rosterRefs:n,farms:o,threshold:f??M,limit:b??_,live:y,force:L,ghostUrl:E,label:k,thresholdSet:f!==null}},et=t=>t.includes("*")||t.includes("?"),nt=t=>t==="PUBLIC"||t==="LOCAL"||t==="PRIVATE",st=t=>t.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,""),P=new Map,it=async(t,e)=>{if(P.has(t))return P.get(t);let n=`${e}/system/indexed-domains.json?pattern=${encodeURIComponent(t)}`,o=await fetch(n);if(!o.ok)throw new Error(`indexed-domains failed: ${o.status}`);let f=await o.json();return P.set(t,f),f},G=async(t,e)=>{t.length||(t=[window.location.hostname]);let n=new Set,o=[];for(let f of t)if(f==="*"||et(f)||nt(f))for(let b of await it(f,e))n.has(b.domain)||(n.add(b.domain),o.push(b));else n.has(f)||(n.add(f),o.push({domain:f,page_count:null}));return o},ot=/^([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+|localhost)(:\d+)?$/,at=/^ROSTER ([A-Za-z0-9.\-:]+\/[a-z0-9-]+)$/,rt=/^REFERENCES ([A-Za-z0-9.\-:]+\/[a-z0-9-]+)$/,B=async t=>{let e=t.indexOf("/"),n=await fetch(`//${t.slice(0,e)}/${t.slice(e+1)}.json`);if(!n.ok)throw new Error(`roster page ${t} failed: ${n.status}`);return n.json()},K=async(t,e,n)=>{if(n.has(t))return;n.add(t);let o;try{o=await B(t)}catch(f){console.warn(f.message);return}for(let f of o.story||[])if(f.type==="roster")for(let b of(f.text||"").split(/\r?\n/)){let d=b.trim();if(!d)continue;let y=d.match(ot);if(y){e.add(y[0]);continue}let L=d.match(at);if(L){await K(L[1],e,n);continue}let E=d.match(rt);if(E)try{let k=await B(E[1]);for(let w of k.story||[])w.type==="reference"&&w.site&&e.add(w.site)}catch(k){console.warn(k.message)}}},lt=async t=>{let e=new Set,n=new Set;for(let o of t)await K(o,e,n);return[...e]},ct=async(t,e)=>e.length?[...t,...await lt(e)]:t,A=new Map,dt=t=>`${window.location.origin}/system/semantic-vectors.json?domain=${encodeURIComponent(t)}`,Z=async t=>{if(A.has(t))return A.get(t);let e=await fetch(dt(t));if(!e.ok)return[];let n=await e.json();return A.set(t,n),n},z=async(t,e)=>{let n=await fetch(`${e}/system/embed.json?text=${encodeURIComponent(t)}`);if(!n.ok)throw new Error(`embed failed: ${n.status}`);return(await n.json()).vector},ft=async(t,e)=>{let o=(await Z(e)).find(f=>f.slug===t);return o?o.vector:null},H=(t,e,{threshold:n,limit:o,excludeSlug:f,excludeDomain:b})=>{let d=[];for(let{domain:y,pages:L}of e)for(let{slug:E,title:k,vector:w}of L){if(E===f&&y===b)continue;let x=0;for(let i=0;i<t.length;i++)x+=t[i]*w[i];x>=n&&d.push({domain:y,slug:E,title:k,score:x})}return d.sort((y,L)=>L.score-y.score),d.slice(0,o)},F=async(t,e)=>{let n=await G(t,e);return(await Promise.all(n.map(async({domain:f})=>({domain:f,pages:await Z(f)})))).filter(f=>f.pages.length>0)},J=t=>`sim-cache-${t}`,mt=t=>{try{let e=JSON.parse(localStorage.getItem(J(t.id))||"null");return e?.text===(t.text||"")?e:null}catch{return null}},O=(t,e)=>{try{localStorage.setItem(J(t.id),JSON.stringify({text:t.text||"",ts:Date.now(),...e}))}catch{}},ut=t=>{let e=Math.floor((Date.now()-t)/1e3);return e<60?`${e}s ago`:e<3600?`${Math.floor(e/60)}m ago`:e<86400?`${Math.floor(e/3600)}h ago`:`${Math.floor(e/86400)}d ago`},I=`
4
4
  .sim-form { display:flex; gap:6px; margin-bottom:8px; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-plugin-similarity",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Federated Wiki plugin \u2014 semantic similarity search across wiki domains using prebuilt vector indexes",
5
5
  "keywords": [
6
6
  "wiki",
@@ -42,8 +42,10 @@
42
42
  "prepublishOnly": "npm run build"
43
43
  },
44
44
  "dependencies": {
45
+ "minisearch": "^7.2.0"
46
+ },
47
+ "optionalDependencies": {
45
48
  "@xenova/transformers": "^2.17.2",
46
- "minisearch": "^7.2.0",
47
49
  "onnxruntime-web": "1.14.0"
48
50
  },
49
51
  "devDependencies": {
@@ -19,6 +19,21 @@ const MODEL = process.env.WIKI_EMBED_MODEL || 'Xenova/bge-small-en-v1.5'
19
19
  const POOLING = process.env.WIKI_EMBED_POOLING || 'cls'
20
20
  const QUANTIZED = process.env.WIKI_EMBED_QUANTIZED === '1'
21
21
 
22
+ // Prefer wiki-plugin-semindex's shared worker-thread embedder when installed:
23
+ // one model instance per process (shared with bulk indexing), inference off
24
+ // the event loop, and similarity's own heavy deps become optional. The
25
+ // sibling path works for npm installs (wiki/node_modules/*) and dev symlinks
26
+ // (~/Code/wiki-plugins/*) alike; the bare require covers other layouts.
27
+ const semindexLib = (() => {
28
+ const path = require('node:path')
29
+ const fs = require('node:fs')
30
+ try {
31
+ const sibling = path.resolve(__dirname, '../../wiki-plugin-semindex/server/embed-lib.js')
32
+ if (fs.existsSync(sibling)) return require(sibling)
33
+ return require('wiki-plugin-semindex/server/embed-lib.js')
34
+ } catch { return null }
35
+ })()
36
+
22
37
  // transformers.js statically imports onnxruntime-node, whose native binary
23
38
  // needs glibc. On musl containers (node:alpine — the public farm) that import
24
39
  // throws "Error loading shared library ld-linux-x86-64.so.2". Probe the
@@ -47,7 +62,13 @@ const getExtractor = () => {
47
62
  console.log('[wiki-plugin-similarity] native onnxruntime unavailable — using WASM backend')
48
63
  }
49
64
  // Dynamic import: @xenova/transformers is ESM-only; this file must stay CJS.
50
- const { pipeline, env } = await import('@xenova/transformers')
65
+ // Since 0.8.0 the heavy deps are optionalDependencies — absent on slim
66
+ // installs, where semindex delegation or WIKI_EMBED_URL is the path.
67
+ const { pipeline, env } = await import('@xenova/transformers').catch(() => {
68
+ throw new Error('no local embedder: @xenova/transformers is not installed. ' +
69
+ 'Install wiki-plugin-semindex, set WIKI_EMBED_URL, or reinstall ' +
70
+ 'wiki-plugin-similarity with optional dependencies.')
71
+ })
51
72
  if (process.env.WIKI_MODEL_CACHE) env.cacheDir = process.env.WIKI_MODEL_CACHE
52
73
  if (env.backends?.onnx?.wasm) env.backends.onnx.wasm.numThreads = 1
53
74
  return pipeline('feature-extraction', MODEL, { quantized: QUANTIZED })
@@ -59,13 +80,15 @@ const getExtractor = () => {
59
80
 
60
81
  // Embed one text → plain number[384], unit-normalised.
61
82
  const embed = async text => {
83
+ if (semindexLib) return semindexLib.embed(text)
62
84
  const extractor = await getExtractor()
63
85
  const out = await extractor(text, { pooling: POOLING, normalize: true })
64
86
  return Array.from(out.data)
65
87
  }
66
88
 
67
89
  // Warm the model in the background (call at startup; failures just defer to
68
- // first request).
69
- const warm = () => { getExtractor().catch(() => {}) }
90
+ // first request). With semindex delegation, don't warm — its worker spawns on
91
+ // demand and self-terminates when idle; pre-warming would just pin ~0.5GB.
92
+ const warm = () => { if (!semindexLib) getExtractor().catch(() => {}) }
70
93
 
71
- module.exports = { embed, warm }
94
+ module.exports = { embed, warm, viaSemindex: !!semindexLib }
package/server/server.js CHANGED
@@ -501,26 +501,47 @@ const startServer = ({ argv, app }) => {
501
501
  })
502
502
 
503
503
  // ── GET /system/build-index.json?domains=…&force=… ────────────────────────
504
- // Heavy embedding is the farm indexer's job proxy when configured, else
505
- // explain where indexing happens.
504
+ // Resolution order: WIKI_INDEXER_URL proxy (explicit config wins) local
505
+ // wiki-plugin-semindex (in-process enqueue, owner/admin only) → 501.
506
+ const semindex = (() => {
507
+ try {
508
+ const sibling = path.resolve(__dirname, '../../wiki-plugin-semindex/server/indexer.js')
509
+ if (fs.existsSync(sibling)) return require(sibling)
510
+ return require('wiki-plugin-semindex/server/indexer.js')
511
+ } catch { return null }
512
+ })()
513
+
506
514
  app.get('/system/build-index.json', async (req, res) => {
507
515
  cors(res)
508
- if (!INDEXER_URL) {
509
- return res.status(501).json({
510
- error: 'no farm indexer configured on this server',
511
- hint: 'Indexing runs on the farm indexer (Pi5 on the Hitchhikers farm); ' +
512
- 'indexes arrive by sync. Set WIKI_INDEXER_URL to enable proxying.',
513
- })
516
+ if (INDEXER_URL) {
517
+ try {
518
+ const qs = new URLSearchParams({
519
+ domains: req.query.domains || '*',
520
+ force: req.query.force || '0',
521
+ })
522
+ return res.json(await getJson(`${INDEXER_URL}?${qs}`))
523
+ } catch (e) {
524
+ return res.status(502).json({ error: `farm indexer unavailable: ${e.message}` })
525
+ }
514
526
  }
515
- try {
516
- const qs = new URLSearchParams({
517
- domains: req.query.domains || '*',
518
- force: req.query.force || '0',
519
- })
520
- res.json(await getJson(`${INDEXER_URL}?${qs}`))
521
- } catch (e) {
522
- res.status(502).json({ error: `farm indexer unavailable: ${e.message}` })
527
+ if (semindex) {
528
+ const sh = app.securityhandler
529
+ const ok = (() => {
530
+ try { return !!sh && (!!sh.isAdmin?.(req) || !!sh.isAuthorized?.(req)) } catch { return false }
531
+ })()
532
+ if (!ok) return res.sendStatus(403)
533
+ if (semindex.MODE !== 'writer') {
534
+ return res.status(409).json({ error: `semindex mode is '${semindex.MODE}' — not a writer` })
535
+ }
536
+ const queued = semindex.enqueueGlobs(req.query.domains || '*', req.query.force === '1')
537
+ return res.json({ queued, ...semindex.status() })
523
538
  }
539
+ res.status(501).json({
540
+ error: 'no indexer available on this server',
541
+ hint: 'Install wiki-plugin-semindex for in-process indexing, or set ' +
542
+ 'WIKI_INDEXER_URL to proxy to a farm indexer; indexes can also ' +
543
+ 'arrive by sync.',
544
+ })
524
545
  })
525
546
 
526
547
  console.log('[wiki-plugin-similarity] routes registered')