uilint-core 0.2.0 → 0.2.3
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/{chunk-FIESH4VM.js → chunk-23BX6XC7.js} +22 -6
- package/dist/chunk-23BX6XC7.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +1 -1
- package/dist/node.d.ts +156 -3
- package/dist/node.js +1494 -10
- package/dist/node.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-FIESH4VM.js.map +0 -1
package/dist/node.js
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
truncateHTML,
|
|
36
36
|
validateStyleGuide,
|
|
37
37
|
validateViolations
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-23BX6XC7.js";
|
|
39
39
|
|
|
40
40
|
// src/logger.ts
|
|
41
41
|
import pc from "picocolors";
|
|
@@ -92,7 +92,7 @@ import { spawn, spawnSync } from "child_process";
|
|
|
92
92
|
import readline from "readline";
|
|
93
93
|
var DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434";
|
|
94
94
|
function sleep(ms) {
|
|
95
|
-
return new Promise((
|
|
95
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
96
96
|
}
|
|
97
97
|
function isInteractiveTTY() {
|
|
98
98
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -102,11 +102,11 @@ async function promptYesNo(question) {
|
|
|
102
102
|
input: process.stdin,
|
|
103
103
|
output: process.stdout
|
|
104
104
|
});
|
|
105
|
-
return await new Promise((
|
|
105
|
+
return await new Promise((resolve2) => {
|
|
106
106
|
rl.question(`${question} (y/N) `, (answer) => {
|
|
107
107
|
rl.close();
|
|
108
108
|
const normalized = (answer || "").trim().toLowerCase();
|
|
109
|
-
|
|
109
|
+
resolve2(normalized === "y" || normalized === "yes");
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
112
|
}
|
|
@@ -149,11 +149,11 @@ async function maybeInstallOllamaWithBrew() {
|
|
|
149
149
|
"Ollama is not installed. Install with Homebrew now?"
|
|
150
150
|
);
|
|
151
151
|
if (!ok) return false;
|
|
152
|
-
await new Promise((
|
|
152
|
+
await new Promise((resolve2, reject) => {
|
|
153
153
|
const child = spawn("brew", ["install", "ollama"], { stdio: "inherit" });
|
|
154
154
|
child.on("error", reject);
|
|
155
155
|
child.on("exit", (code) => {
|
|
156
|
-
if (code === 0)
|
|
156
|
+
if (code === 0) resolve2();
|
|
157
157
|
else reject(new Error(`brew install ollama failed (exit ${code})`));
|
|
158
158
|
});
|
|
159
159
|
});
|
|
@@ -226,11 +226,11 @@ async function ensureOllamaModelPulled(options) {
|
|
|
226
226
|
const baseUrl = options?.baseUrl || DEFAULT_OLLAMA_BASE_URL;
|
|
227
227
|
const tags = await fetchOllamaTags(baseUrl);
|
|
228
228
|
if (tags.includes(desired)) return;
|
|
229
|
-
await new Promise((
|
|
229
|
+
await new Promise((resolve2, reject) => {
|
|
230
230
|
const child = spawn("ollama", ["pull", desired], { stdio: "inherit" });
|
|
231
231
|
child.on("error", reject);
|
|
232
232
|
child.on("exit", (code) => {
|
|
233
|
-
if (code === 0)
|
|
233
|
+
if (code === 0) resolve2();
|
|
234
234
|
else reject(new Error(`ollama pull ${desired} failed (exit ${code})`));
|
|
235
235
|
});
|
|
236
236
|
});
|
|
@@ -295,11 +295,11 @@ function isJSON(str) {
|
|
|
295
295
|
}
|
|
296
296
|
async function readStdin() {
|
|
297
297
|
const chunks = [];
|
|
298
|
-
return new Promise((
|
|
298
|
+
return new Promise((resolve2, reject) => {
|
|
299
299
|
process.stdin.on("data", (chunk) => {
|
|
300
300
|
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
301
301
|
});
|
|
302
|
-
process.stdin.on("end", () =>
|
|
302
|
+
process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
|
|
303
303
|
process.stdin.on("error", reject);
|
|
304
304
|
});
|
|
305
305
|
}
|
|
@@ -481,10 +481,1493 @@ function addColorTokens(out, colors) {
|
|
|
481
481
|
}
|
|
482
482
|
}
|
|
483
483
|
}
|
|
484
|
+
|
|
485
|
+
// ../../node_modules/.pnpm/ollama@0.6.3/node_modules/ollama/dist/index.mjs
|
|
486
|
+
import fs, { promises } from "fs";
|
|
487
|
+
import { resolve } from "path";
|
|
488
|
+
|
|
489
|
+
// ../../node_modules/.pnpm/whatwg-fetch@3.6.20/node_modules/whatwg-fetch/fetch.js
|
|
490
|
+
var g = typeof globalThis !== "undefined" && globalThis || typeof self !== "undefined" && self || // eslint-disable-next-line no-undef
|
|
491
|
+
typeof global !== "undefined" && global || {};
|
|
492
|
+
var support = {
|
|
493
|
+
searchParams: "URLSearchParams" in g,
|
|
494
|
+
iterable: "Symbol" in g && "iterator" in Symbol,
|
|
495
|
+
blob: "FileReader" in g && "Blob" in g && (function() {
|
|
496
|
+
try {
|
|
497
|
+
new Blob();
|
|
498
|
+
return true;
|
|
499
|
+
} catch (e) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
})(),
|
|
503
|
+
formData: "FormData" in g,
|
|
504
|
+
arrayBuffer: "ArrayBuffer" in g
|
|
505
|
+
};
|
|
506
|
+
function isDataView(obj) {
|
|
507
|
+
return obj && DataView.prototype.isPrototypeOf(obj);
|
|
508
|
+
}
|
|
509
|
+
if (support.arrayBuffer) {
|
|
510
|
+
viewClasses = [
|
|
511
|
+
"[object Int8Array]",
|
|
512
|
+
"[object Uint8Array]",
|
|
513
|
+
"[object Uint8ClampedArray]",
|
|
514
|
+
"[object Int16Array]",
|
|
515
|
+
"[object Uint16Array]",
|
|
516
|
+
"[object Int32Array]",
|
|
517
|
+
"[object Uint32Array]",
|
|
518
|
+
"[object Float32Array]",
|
|
519
|
+
"[object Float64Array]"
|
|
520
|
+
];
|
|
521
|
+
isArrayBufferView = ArrayBuffer.isView || function(obj) {
|
|
522
|
+
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1;
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
var viewClasses;
|
|
526
|
+
var isArrayBufferView;
|
|
527
|
+
function normalizeName(name) {
|
|
528
|
+
if (typeof name !== "string") {
|
|
529
|
+
name = String(name);
|
|
530
|
+
}
|
|
531
|
+
if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === "") {
|
|
532
|
+
throw new TypeError('Invalid character in header field name: "' + name + '"');
|
|
533
|
+
}
|
|
534
|
+
return name.toLowerCase();
|
|
535
|
+
}
|
|
536
|
+
function normalizeValue(value) {
|
|
537
|
+
if (typeof value !== "string") {
|
|
538
|
+
value = String(value);
|
|
539
|
+
}
|
|
540
|
+
return value;
|
|
541
|
+
}
|
|
542
|
+
function iteratorFor(items) {
|
|
543
|
+
var iterator = {
|
|
544
|
+
next: function() {
|
|
545
|
+
var value = items.shift();
|
|
546
|
+
return { done: value === void 0, value };
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
if (support.iterable) {
|
|
550
|
+
iterator[Symbol.iterator] = function() {
|
|
551
|
+
return iterator;
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
return iterator;
|
|
555
|
+
}
|
|
556
|
+
function Headers2(headers) {
|
|
557
|
+
this.map = {};
|
|
558
|
+
if (headers instanceof Headers2) {
|
|
559
|
+
headers.forEach(function(value, name) {
|
|
560
|
+
this.append(name, value);
|
|
561
|
+
}, this);
|
|
562
|
+
} else if (Array.isArray(headers)) {
|
|
563
|
+
headers.forEach(function(header) {
|
|
564
|
+
if (header.length != 2) {
|
|
565
|
+
throw new TypeError("Headers constructor: expected name/value pair to be length 2, found" + header.length);
|
|
566
|
+
}
|
|
567
|
+
this.append(header[0], header[1]);
|
|
568
|
+
}, this);
|
|
569
|
+
} else if (headers) {
|
|
570
|
+
Object.getOwnPropertyNames(headers).forEach(function(name) {
|
|
571
|
+
this.append(name, headers[name]);
|
|
572
|
+
}, this);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
Headers2.prototype.append = function(name, value) {
|
|
576
|
+
name = normalizeName(name);
|
|
577
|
+
value = normalizeValue(value);
|
|
578
|
+
var oldValue = this.map[name];
|
|
579
|
+
this.map[name] = oldValue ? oldValue + ", " + value : value;
|
|
580
|
+
};
|
|
581
|
+
Headers2.prototype["delete"] = function(name) {
|
|
582
|
+
delete this.map[normalizeName(name)];
|
|
583
|
+
};
|
|
584
|
+
Headers2.prototype.get = function(name) {
|
|
585
|
+
name = normalizeName(name);
|
|
586
|
+
return this.has(name) ? this.map[name] : null;
|
|
587
|
+
};
|
|
588
|
+
Headers2.prototype.has = function(name) {
|
|
589
|
+
return this.map.hasOwnProperty(normalizeName(name));
|
|
590
|
+
};
|
|
591
|
+
Headers2.prototype.set = function(name, value) {
|
|
592
|
+
this.map[normalizeName(name)] = normalizeValue(value);
|
|
593
|
+
};
|
|
594
|
+
Headers2.prototype.forEach = function(callback, thisArg) {
|
|
595
|
+
for (var name in this.map) {
|
|
596
|
+
if (this.map.hasOwnProperty(name)) {
|
|
597
|
+
callback.call(thisArg, this.map[name], name, this);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
Headers2.prototype.keys = function() {
|
|
602
|
+
var items = [];
|
|
603
|
+
this.forEach(function(value, name) {
|
|
604
|
+
items.push(name);
|
|
605
|
+
});
|
|
606
|
+
return iteratorFor(items);
|
|
607
|
+
};
|
|
608
|
+
Headers2.prototype.values = function() {
|
|
609
|
+
var items = [];
|
|
610
|
+
this.forEach(function(value) {
|
|
611
|
+
items.push(value);
|
|
612
|
+
});
|
|
613
|
+
return iteratorFor(items);
|
|
614
|
+
};
|
|
615
|
+
Headers2.prototype.entries = function() {
|
|
616
|
+
var items = [];
|
|
617
|
+
this.forEach(function(value, name) {
|
|
618
|
+
items.push([name, value]);
|
|
619
|
+
});
|
|
620
|
+
return iteratorFor(items);
|
|
621
|
+
};
|
|
622
|
+
if (support.iterable) {
|
|
623
|
+
Headers2.prototype[Symbol.iterator] = Headers2.prototype.entries;
|
|
624
|
+
}
|
|
625
|
+
function consumed(body) {
|
|
626
|
+
if (body._noBody) return;
|
|
627
|
+
if (body.bodyUsed) {
|
|
628
|
+
return Promise.reject(new TypeError("Already read"));
|
|
629
|
+
}
|
|
630
|
+
body.bodyUsed = true;
|
|
631
|
+
}
|
|
632
|
+
function fileReaderReady(reader) {
|
|
633
|
+
return new Promise(function(resolve2, reject) {
|
|
634
|
+
reader.onload = function() {
|
|
635
|
+
resolve2(reader.result);
|
|
636
|
+
};
|
|
637
|
+
reader.onerror = function() {
|
|
638
|
+
reject(reader.error);
|
|
639
|
+
};
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
function readBlobAsArrayBuffer(blob) {
|
|
643
|
+
var reader = new FileReader();
|
|
644
|
+
var promise = fileReaderReady(reader);
|
|
645
|
+
reader.readAsArrayBuffer(blob);
|
|
646
|
+
return promise;
|
|
647
|
+
}
|
|
648
|
+
function readBlobAsText(blob) {
|
|
649
|
+
var reader = new FileReader();
|
|
650
|
+
var promise = fileReaderReady(reader);
|
|
651
|
+
var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type);
|
|
652
|
+
var encoding = match ? match[1] : "utf-8";
|
|
653
|
+
reader.readAsText(blob, encoding);
|
|
654
|
+
return promise;
|
|
655
|
+
}
|
|
656
|
+
function readArrayBufferAsText(buf) {
|
|
657
|
+
var view = new Uint8Array(buf);
|
|
658
|
+
var chars = new Array(view.length);
|
|
659
|
+
for (var i = 0; i < view.length; i++) {
|
|
660
|
+
chars[i] = String.fromCharCode(view[i]);
|
|
661
|
+
}
|
|
662
|
+
return chars.join("");
|
|
663
|
+
}
|
|
664
|
+
function bufferClone(buf) {
|
|
665
|
+
if (buf.slice) {
|
|
666
|
+
return buf.slice(0);
|
|
667
|
+
} else {
|
|
668
|
+
var view = new Uint8Array(buf.byteLength);
|
|
669
|
+
view.set(new Uint8Array(buf));
|
|
670
|
+
return view.buffer;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function Body() {
|
|
674
|
+
this.bodyUsed = false;
|
|
675
|
+
this._initBody = function(body) {
|
|
676
|
+
this.bodyUsed = this.bodyUsed;
|
|
677
|
+
this._bodyInit = body;
|
|
678
|
+
if (!body) {
|
|
679
|
+
this._noBody = true;
|
|
680
|
+
this._bodyText = "";
|
|
681
|
+
} else if (typeof body === "string") {
|
|
682
|
+
this._bodyText = body;
|
|
683
|
+
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
|
|
684
|
+
this._bodyBlob = body;
|
|
685
|
+
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
|
|
686
|
+
this._bodyFormData = body;
|
|
687
|
+
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
|
688
|
+
this._bodyText = body.toString();
|
|
689
|
+
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
|
|
690
|
+
this._bodyArrayBuffer = bufferClone(body.buffer);
|
|
691
|
+
this._bodyInit = new Blob([this._bodyArrayBuffer]);
|
|
692
|
+
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
|
|
693
|
+
this._bodyArrayBuffer = bufferClone(body);
|
|
694
|
+
} else {
|
|
695
|
+
this._bodyText = body = Object.prototype.toString.call(body);
|
|
696
|
+
}
|
|
697
|
+
if (!this.headers.get("content-type")) {
|
|
698
|
+
if (typeof body === "string") {
|
|
699
|
+
this.headers.set("content-type", "text/plain;charset=UTF-8");
|
|
700
|
+
} else if (this._bodyBlob && this._bodyBlob.type) {
|
|
701
|
+
this.headers.set("content-type", this._bodyBlob.type);
|
|
702
|
+
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
|
703
|
+
this.headers.set("content-type", "application/x-www-form-urlencoded;charset=UTF-8");
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
if (support.blob) {
|
|
708
|
+
this.blob = function() {
|
|
709
|
+
var rejected = consumed(this);
|
|
710
|
+
if (rejected) {
|
|
711
|
+
return rejected;
|
|
712
|
+
}
|
|
713
|
+
if (this._bodyBlob) {
|
|
714
|
+
return Promise.resolve(this._bodyBlob);
|
|
715
|
+
} else if (this._bodyArrayBuffer) {
|
|
716
|
+
return Promise.resolve(new Blob([this._bodyArrayBuffer]));
|
|
717
|
+
} else if (this._bodyFormData) {
|
|
718
|
+
throw new Error("could not read FormData body as blob");
|
|
719
|
+
} else {
|
|
720
|
+
return Promise.resolve(new Blob([this._bodyText]));
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
this.arrayBuffer = function() {
|
|
725
|
+
if (this._bodyArrayBuffer) {
|
|
726
|
+
var isConsumed = consumed(this);
|
|
727
|
+
if (isConsumed) {
|
|
728
|
+
return isConsumed;
|
|
729
|
+
} else if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
|
|
730
|
+
return Promise.resolve(
|
|
731
|
+
this._bodyArrayBuffer.buffer.slice(
|
|
732
|
+
this._bodyArrayBuffer.byteOffset,
|
|
733
|
+
this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength
|
|
734
|
+
)
|
|
735
|
+
);
|
|
736
|
+
} else {
|
|
737
|
+
return Promise.resolve(this._bodyArrayBuffer);
|
|
738
|
+
}
|
|
739
|
+
} else if (support.blob) {
|
|
740
|
+
return this.blob().then(readBlobAsArrayBuffer);
|
|
741
|
+
} else {
|
|
742
|
+
throw new Error("could not read as ArrayBuffer");
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
this.text = function() {
|
|
746
|
+
var rejected = consumed(this);
|
|
747
|
+
if (rejected) {
|
|
748
|
+
return rejected;
|
|
749
|
+
}
|
|
750
|
+
if (this._bodyBlob) {
|
|
751
|
+
return readBlobAsText(this._bodyBlob);
|
|
752
|
+
} else if (this._bodyArrayBuffer) {
|
|
753
|
+
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer));
|
|
754
|
+
} else if (this._bodyFormData) {
|
|
755
|
+
throw new Error("could not read FormData body as text");
|
|
756
|
+
} else {
|
|
757
|
+
return Promise.resolve(this._bodyText);
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
if (support.formData) {
|
|
761
|
+
this.formData = function() {
|
|
762
|
+
return this.text().then(decode);
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
this.json = function() {
|
|
766
|
+
return this.text().then(JSON.parse);
|
|
767
|
+
};
|
|
768
|
+
return this;
|
|
769
|
+
}
|
|
770
|
+
var methods = ["CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"];
|
|
771
|
+
function normalizeMethod(method) {
|
|
772
|
+
var upcased = method.toUpperCase();
|
|
773
|
+
return methods.indexOf(upcased) > -1 ? upcased : method;
|
|
774
|
+
}
|
|
775
|
+
function Request(input, options) {
|
|
776
|
+
if (!(this instanceof Request)) {
|
|
777
|
+
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');
|
|
778
|
+
}
|
|
779
|
+
options = options || {};
|
|
780
|
+
var body = options.body;
|
|
781
|
+
if (input instanceof Request) {
|
|
782
|
+
if (input.bodyUsed) {
|
|
783
|
+
throw new TypeError("Already read");
|
|
784
|
+
}
|
|
785
|
+
this.url = input.url;
|
|
786
|
+
this.credentials = input.credentials;
|
|
787
|
+
if (!options.headers) {
|
|
788
|
+
this.headers = new Headers2(input.headers);
|
|
789
|
+
}
|
|
790
|
+
this.method = input.method;
|
|
791
|
+
this.mode = input.mode;
|
|
792
|
+
this.signal = input.signal;
|
|
793
|
+
if (!body && input._bodyInit != null) {
|
|
794
|
+
body = input._bodyInit;
|
|
795
|
+
input.bodyUsed = true;
|
|
796
|
+
}
|
|
797
|
+
} else {
|
|
798
|
+
this.url = String(input);
|
|
799
|
+
}
|
|
800
|
+
this.credentials = options.credentials || this.credentials || "same-origin";
|
|
801
|
+
if (options.headers || !this.headers) {
|
|
802
|
+
this.headers = new Headers2(options.headers);
|
|
803
|
+
}
|
|
804
|
+
this.method = normalizeMethod(options.method || this.method || "GET");
|
|
805
|
+
this.mode = options.mode || this.mode || null;
|
|
806
|
+
this.signal = options.signal || this.signal || (function() {
|
|
807
|
+
if ("AbortController" in g) {
|
|
808
|
+
var ctrl = new AbortController();
|
|
809
|
+
return ctrl.signal;
|
|
810
|
+
}
|
|
811
|
+
})();
|
|
812
|
+
this.referrer = null;
|
|
813
|
+
if ((this.method === "GET" || this.method === "HEAD") && body) {
|
|
814
|
+
throw new TypeError("Body not allowed for GET or HEAD requests");
|
|
815
|
+
}
|
|
816
|
+
this._initBody(body);
|
|
817
|
+
if (this.method === "GET" || this.method === "HEAD") {
|
|
818
|
+
if (options.cache === "no-store" || options.cache === "no-cache") {
|
|
819
|
+
var reParamSearch = /([?&])_=[^&]*/;
|
|
820
|
+
if (reParamSearch.test(this.url)) {
|
|
821
|
+
this.url = this.url.replace(reParamSearch, "$1_=" + (/* @__PURE__ */ new Date()).getTime());
|
|
822
|
+
} else {
|
|
823
|
+
var reQueryString = /\?/;
|
|
824
|
+
this.url += (reQueryString.test(this.url) ? "&" : "?") + "_=" + (/* @__PURE__ */ new Date()).getTime();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
Request.prototype.clone = function() {
|
|
830
|
+
return new Request(this, { body: this._bodyInit });
|
|
831
|
+
};
|
|
832
|
+
function decode(body) {
|
|
833
|
+
var form = new FormData();
|
|
834
|
+
body.trim().split("&").forEach(function(bytes) {
|
|
835
|
+
if (bytes) {
|
|
836
|
+
var split = bytes.split("=");
|
|
837
|
+
var name = split.shift().replace(/\+/g, " ");
|
|
838
|
+
var value = split.join("=").replace(/\+/g, " ");
|
|
839
|
+
form.append(decodeURIComponent(name), decodeURIComponent(value));
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
return form;
|
|
843
|
+
}
|
|
844
|
+
function parseHeaders(rawHeaders) {
|
|
845
|
+
var headers = new Headers2();
|
|
846
|
+
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, " ");
|
|
847
|
+
preProcessedHeaders.split("\r").map(function(header) {
|
|
848
|
+
return header.indexOf("\n") === 0 ? header.substr(1, header.length) : header;
|
|
849
|
+
}).forEach(function(line) {
|
|
850
|
+
var parts = line.split(":");
|
|
851
|
+
var key = parts.shift().trim();
|
|
852
|
+
if (key) {
|
|
853
|
+
var value = parts.join(":").trim();
|
|
854
|
+
try {
|
|
855
|
+
headers.append(key, value);
|
|
856
|
+
} catch (error) {
|
|
857
|
+
console.warn("Response " + error.message);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
return headers;
|
|
862
|
+
}
|
|
863
|
+
Body.call(Request.prototype);
|
|
864
|
+
function Response(bodyInit, options) {
|
|
865
|
+
if (!(this instanceof Response)) {
|
|
866
|
+
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');
|
|
867
|
+
}
|
|
868
|
+
if (!options) {
|
|
869
|
+
options = {};
|
|
870
|
+
}
|
|
871
|
+
this.type = "default";
|
|
872
|
+
this.status = options.status === void 0 ? 200 : options.status;
|
|
873
|
+
if (this.status < 200 || this.status > 599) {
|
|
874
|
+
throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].");
|
|
875
|
+
}
|
|
876
|
+
this.ok = this.status >= 200 && this.status < 300;
|
|
877
|
+
this.statusText = options.statusText === void 0 ? "" : "" + options.statusText;
|
|
878
|
+
this.headers = new Headers2(options.headers);
|
|
879
|
+
this.url = options.url || "";
|
|
880
|
+
this._initBody(bodyInit);
|
|
881
|
+
}
|
|
882
|
+
Body.call(Response.prototype);
|
|
883
|
+
Response.prototype.clone = function() {
|
|
884
|
+
return new Response(this._bodyInit, {
|
|
885
|
+
status: this.status,
|
|
886
|
+
statusText: this.statusText,
|
|
887
|
+
headers: new Headers2(this.headers),
|
|
888
|
+
url: this.url
|
|
889
|
+
});
|
|
890
|
+
};
|
|
891
|
+
Response.error = function() {
|
|
892
|
+
var response = new Response(null, { status: 200, statusText: "" });
|
|
893
|
+
response.ok = false;
|
|
894
|
+
response.status = 0;
|
|
895
|
+
response.type = "error";
|
|
896
|
+
return response;
|
|
897
|
+
};
|
|
898
|
+
var redirectStatuses = [301, 302, 303, 307, 308];
|
|
899
|
+
Response.redirect = function(url, status) {
|
|
900
|
+
if (redirectStatuses.indexOf(status) === -1) {
|
|
901
|
+
throw new RangeError("Invalid status code");
|
|
902
|
+
}
|
|
903
|
+
return new Response(null, { status, headers: { location: url } });
|
|
904
|
+
};
|
|
905
|
+
var DOMException = g.DOMException;
|
|
906
|
+
try {
|
|
907
|
+
new DOMException();
|
|
908
|
+
} catch (err) {
|
|
909
|
+
DOMException = function(message, name) {
|
|
910
|
+
this.message = message;
|
|
911
|
+
this.name = name;
|
|
912
|
+
var error = Error(message);
|
|
913
|
+
this.stack = error.stack;
|
|
914
|
+
};
|
|
915
|
+
DOMException.prototype = Object.create(Error.prototype);
|
|
916
|
+
DOMException.prototype.constructor = DOMException;
|
|
917
|
+
}
|
|
918
|
+
function fetch2(input, init) {
|
|
919
|
+
return new Promise(function(resolve2, reject) {
|
|
920
|
+
var request = new Request(input, init);
|
|
921
|
+
if (request.signal && request.signal.aborted) {
|
|
922
|
+
return reject(new DOMException("Aborted", "AbortError"));
|
|
923
|
+
}
|
|
924
|
+
var xhr = new XMLHttpRequest();
|
|
925
|
+
function abortXhr() {
|
|
926
|
+
xhr.abort();
|
|
927
|
+
}
|
|
928
|
+
xhr.onload = function() {
|
|
929
|
+
var options = {
|
|
930
|
+
statusText: xhr.statusText,
|
|
931
|
+
headers: parseHeaders(xhr.getAllResponseHeaders() || "")
|
|
932
|
+
};
|
|
933
|
+
if (request.url.indexOf("file://") === 0 && (xhr.status < 200 || xhr.status > 599)) {
|
|
934
|
+
options.status = 200;
|
|
935
|
+
} else {
|
|
936
|
+
options.status = xhr.status;
|
|
937
|
+
}
|
|
938
|
+
options.url = "responseURL" in xhr ? xhr.responseURL : options.headers.get("X-Request-URL");
|
|
939
|
+
var body = "response" in xhr ? xhr.response : xhr.responseText;
|
|
940
|
+
setTimeout(function() {
|
|
941
|
+
resolve2(new Response(body, options));
|
|
942
|
+
}, 0);
|
|
943
|
+
};
|
|
944
|
+
xhr.onerror = function() {
|
|
945
|
+
setTimeout(function() {
|
|
946
|
+
reject(new TypeError("Network request failed"));
|
|
947
|
+
}, 0);
|
|
948
|
+
};
|
|
949
|
+
xhr.ontimeout = function() {
|
|
950
|
+
setTimeout(function() {
|
|
951
|
+
reject(new TypeError("Network request timed out"));
|
|
952
|
+
}, 0);
|
|
953
|
+
};
|
|
954
|
+
xhr.onabort = function() {
|
|
955
|
+
setTimeout(function() {
|
|
956
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
957
|
+
}, 0);
|
|
958
|
+
};
|
|
959
|
+
function fixUrl(url) {
|
|
960
|
+
try {
|
|
961
|
+
return url === "" && g.location.href ? g.location.href : url;
|
|
962
|
+
} catch (e) {
|
|
963
|
+
return url;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
xhr.open(request.method, fixUrl(request.url), true);
|
|
967
|
+
if (request.credentials === "include") {
|
|
968
|
+
xhr.withCredentials = true;
|
|
969
|
+
} else if (request.credentials === "omit") {
|
|
970
|
+
xhr.withCredentials = false;
|
|
971
|
+
}
|
|
972
|
+
if ("responseType" in xhr) {
|
|
973
|
+
if (support.blob) {
|
|
974
|
+
xhr.responseType = "blob";
|
|
975
|
+
} else if (support.arrayBuffer) {
|
|
976
|
+
xhr.responseType = "arraybuffer";
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (init && typeof init.headers === "object" && !(init.headers instanceof Headers2 || g.Headers && init.headers instanceof g.Headers)) {
|
|
980
|
+
var names = [];
|
|
981
|
+
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
|
|
982
|
+
names.push(normalizeName(name));
|
|
983
|
+
xhr.setRequestHeader(name, normalizeValue(init.headers[name]));
|
|
984
|
+
});
|
|
985
|
+
request.headers.forEach(function(value, name) {
|
|
986
|
+
if (names.indexOf(name) === -1) {
|
|
987
|
+
xhr.setRequestHeader(name, value);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
} else {
|
|
991
|
+
request.headers.forEach(function(value, name) {
|
|
992
|
+
xhr.setRequestHeader(name, value);
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
if (request.signal) {
|
|
996
|
+
request.signal.addEventListener("abort", abortXhr);
|
|
997
|
+
xhr.onreadystatechange = function() {
|
|
998
|
+
if (xhr.readyState === 4) {
|
|
999
|
+
request.signal.removeEventListener("abort", abortXhr);
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
xhr.send(typeof request._bodyInit === "undefined" ? null : request._bodyInit);
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
fetch2.polyfill = true;
|
|
1007
|
+
if (!g.fetch) {
|
|
1008
|
+
g.fetch = fetch2;
|
|
1009
|
+
g.Headers = Headers2;
|
|
1010
|
+
g.Request = Request;
|
|
1011
|
+
g.Response = Response;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// ../../node_modules/.pnpm/ollama@0.6.3/node_modules/ollama/dist/browser.mjs
|
|
1015
|
+
var defaultPort = "11434";
|
|
1016
|
+
var defaultHost = `http://127.0.0.1:${defaultPort}`;
|
|
1017
|
+
var version = "0.6.3";
|
|
1018
|
+
var __defProp$1 = Object.defineProperty;
|
|
1019
|
+
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1020
|
+
var __publicField$1 = (obj, key, value) => {
|
|
1021
|
+
__defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1022
|
+
return value;
|
|
1023
|
+
};
|
|
1024
|
+
var ResponseError = class _ResponseError extends Error {
|
|
1025
|
+
constructor(error, status_code) {
|
|
1026
|
+
super(error);
|
|
1027
|
+
this.error = error;
|
|
1028
|
+
this.status_code = status_code;
|
|
1029
|
+
this.name = "ResponseError";
|
|
1030
|
+
if (Error.captureStackTrace) {
|
|
1031
|
+
Error.captureStackTrace(this, _ResponseError);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
var AbortableAsyncIterator = class {
|
|
1036
|
+
constructor(abortController, itr, doneCallback) {
|
|
1037
|
+
__publicField$1(this, "abortController");
|
|
1038
|
+
__publicField$1(this, "itr");
|
|
1039
|
+
__publicField$1(this, "doneCallback");
|
|
1040
|
+
this.abortController = abortController;
|
|
1041
|
+
this.itr = itr;
|
|
1042
|
+
this.doneCallback = doneCallback;
|
|
1043
|
+
}
|
|
1044
|
+
abort() {
|
|
1045
|
+
this.abortController.abort();
|
|
1046
|
+
}
|
|
1047
|
+
async *[Symbol.asyncIterator]() {
|
|
1048
|
+
for await (const message of this.itr) {
|
|
1049
|
+
if ("error" in message) {
|
|
1050
|
+
throw new Error(message.error);
|
|
1051
|
+
}
|
|
1052
|
+
yield message;
|
|
1053
|
+
if (message.done || message.status === "success") {
|
|
1054
|
+
this.doneCallback();
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
throw new Error("Did not receive done or success response in stream.");
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
var checkOk = async (response) => {
|
|
1062
|
+
if (response.ok) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
let message = `Error ${response.status}: ${response.statusText}`;
|
|
1066
|
+
let errorData = null;
|
|
1067
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
1068
|
+
try {
|
|
1069
|
+
errorData = await response.json();
|
|
1070
|
+
message = errorData.error || message;
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
console.log("Failed to parse error response as JSON");
|
|
1073
|
+
}
|
|
1074
|
+
} else {
|
|
1075
|
+
try {
|
|
1076
|
+
console.log("Getting text from response");
|
|
1077
|
+
const textResponse = await response.text();
|
|
1078
|
+
message = textResponse || message;
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
console.log("Failed to get text from error response");
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
throw new ResponseError(message, response.status);
|
|
1084
|
+
};
|
|
1085
|
+
function getPlatform() {
|
|
1086
|
+
if (typeof window !== "undefined" && window.navigator) {
|
|
1087
|
+
const nav = navigator;
|
|
1088
|
+
if ("userAgentData" in nav && nav.userAgentData?.platform) {
|
|
1089
|
+
return `${nav.userAgentData.platform.toLowerCase()} Browser/${navigator.userAgent};`;
|
|
1090
|
+
}
|
|
1091
|
+
if (navigator.platform) {
|
|
1092
|
+
return `${navigator.platform.toLowerCase()} Browser/${navigator.userAgent};`;
|
|
1093
|
+
}
|
|
1094
|
+
return `unknown Browser/${navigator.userAgent};`;
|
|
1095
|
+
} else if (typeof process !== "undefined") {
|
|
1096
|
+
return `${process.arch} ${process.platform} Node.js/${process.version}`;
|
|
1097
|
+
}
|
|
1098
|
+
return "";
|
|
1099
|
+
}
|
|
1100
|
+
function normalizeHeaders(headers) {
|
|
1101
|
+
if (headers instanceof Headers) {
|
|
1102
|
+
const obj = {};
|
|
1103
|
+
headers.forEach((value, key) => {
|
|
1104
|
+
obj[key] = value;
|
|
1105
|
+
});
|
|
1106
|
+
return obj;
|
|
1107
|
+
} else if (Array.isArray(headers)) {
|
|
1108
|
+
return Object.fromEntries(headers);
|
|
1109
|
+
} else {
|
|
1110
|
+
return headers || {};
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
var readEnvVar = (obj, key) => {
|
|
1114
|
+
return obj[key];
|
|
1115
|
+
};
|
|
1116
|
+
var fetchWithHeaders = async (fetch3, url, options = {}) => {
|
|
1117
|
+
const defaultHeaders = {
|
|
1118
|
+
"Content-Type": "application/json",
|
|
1119
|
+
Accept: "application/json",
|
|
1120
|
+
"User-Agent": `ollama-js/${version} (${getPlatform()})`
|
|
1121
|
+
};
|
|
1122
|
+
options.headers = normalizeHeaders(options.headers);
|
|
1123
|
+
try {
|
|
1124
|
+
const parsed = new URL(url);
|
|
1125
|
+
if (parsed.protocol === "https:" && parsed.hostname === "ollama.com") {
|
|
1126
|
+
const apiKey = typeof process === "object" && process !== null && typeof process.env === "object" && process.env !== null ? readEnvVar(process.env, "OLLAMA_API_KEY") : void 0;
|
|
1127
|
+
const authorization = options.headers["authorization"] || options.headers["Authorization"];
|
|
1128
|
+
if (!authorization && apiKey) {
|
|
1129
|
+
options.headers["Authorization"] = `Bearer ${apiKey}`;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
console.error("error parsing url", error);
|
|
1134
|
+
}
|
|
1135
|
+
const customHeaders = Object.fromEntries(
|
|
1136
|
+
Object.entries(options.headers).filter(
|
|
1137
|
+
([key]) => !Object.keys(defaultHeaders).some(
|
|
1138
|
+
(defaultKey) => defaultKey.toLowerCase() === key.toLowerCase()
|
|
1139
|
+
)
|
|
1140
|
+
)
|
|
1141
|
+
);
|
|
1142
|
+
options.headers = {
|
|
1143
|
+
...defaultHeaders,
|
|
1144
|
+
...customHeaders
|
|
1145
|
+
};
|
|
1146
|
+
return fetch3(url, options);
|
|
1147
|
+
};
|
|
1148
|
+
var get = async (fetch3, host, options) => {
|
|
1149
|
+
const response = await fetchWithHeaders(fetch3, host, {
|
|
1150
|
+
headers: options?.headers
|
|
1151
|
+
});
|
|
1152
|
+
await checkOk(response);
|
|
1153
|
+
return response;
|
|
1154
|
+
};
|
|
1155
|
+
var post = async (fetch3, host, data, options) => {
|
|
1156
|
+
const isRecord = (input) => {
|
|
1157
|
+
return input !== null && typeof input === "object" && !Array.isArray(input);
|
|
1158
|
+
};
|
|
1159
|
+
const formattedData = isRecord(data) ? JSON.stringify(data) : data;
|
|
1160
|
+
const response = await fetchWithHeaders(fetch3, host, {
|
|
1161
|
+
method: "POST",
|
|
1162
|
+
body: formattedData,
|
|
1163
|
+
signal: options?.signal,
|
|
1164
|
+
headers: options?.headers
|
|
1165
|
+
});
|
|
1166
|
+
await checkOk(response);
|
|
1167
|
+
return response;
|
|
1168
|
+
};
|
|
1169
|
+
var del = async (fetch3, host, data, options) => {
|
|
1170
|
+
const response = await fetchWithHeaders(fetch3, host, {
|
|
1171
|
+
method: "DELETE",
|
|
1172
|
+
body: JSON.stringify(data),
|
|
1173
|
+
headers: options?.headers
|
|
1174
|
+
});
|
|
1175
|
+
await checkOk(response);
|
|
1176
|
+
return response;
|
|
1177
|
+
};
|
|
1178
|
+
var parseJSON = async function* (itr) {
|
|
1179
|
+
const decoder = new TextDecoder("utf-8");
|
|
1180
|
+
let buffer = "";
|
|
1181
|
+
const reader = itr.getReader();
|
|
1182
|
+
while (true) {
|
|
1183
|
+
const { done, value: chunk } = await reader.read();
|
|
1184
|
+
if (done) {
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
1188
|
+
const parts = buffer.split("\n");
|
|
1189
|
+
buffer = parts.pop() ?? "";
|
|
1190
|
+
for (const part of parts) {
|
|
1191
|
+
try {
|
|
1192
|
+
yield JSON.parse(part);
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
console.warn("invalid json: ", part);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
buffer += decoder.decode();
|
|
1199
|
+
for (const part of buffer.split("\n").filter((p) => p !== "")) {
|
|
1200
|
+
try {
|
|
1201
|
+
yield JSON.parse(part);
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
console.warn("invalid json: ", part);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
var formatHost = (host) => {
|
|
1208
|
+
if (!host) {
|
|
1209
|
+
return defaultHost;
|
|
1210
|
+
}
|
|
1211
|
+
let isExplicitProtocol = host.includes("://");
|
|
1212
|
+
if (host.startsWith(":")) {
|
|
1213
|
+
host = `http://127.0.0.1${host}`;
|
|
1214
|
+
isExplicitProtocol = true;
|
|
1215
|
+
}
|
|
1216
|
+
if (!isExplicitProtocol) {
|
|
1217
|
+
host = `http://${host}`;
|
|
1218
|
+
}
|
|
1219
|
+
const url = new URL(host);
|
|
1220
|
+
let port = url.port;
|
|
1221
|
+
if (!port) {
|
|
1222
|
+
if (!isExplicitProtocol) {
|
|
1223
|
+
port = defaultPort;
|
|
1224
|
+
} else {
|
|
1225
|
+
port = url.protocol === "https:" ? "443" : "80";
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
let auth = "";
|
|
1229
|
+
if (url.username) {
|
|
1230
|
+
auth = url.username;
|
|
1231
|
+
if (url.password) {
|
|
1232
|
+
auth += `:${url.password}`;
|
|
1233
|
+
}
|
|
1234
|
+
auth += "@";
|
|
1235
|
+
}
|
|
1236
|
+
let formattedHost = `${url.protocol}//${auth}${url.hostname}:${port}${url.pathname}`;
|
|
1237
|
+
if (formattedHost.endsWith("/")) {
|
|
1238
|
+
formattedHost = formattedHost.slice(0, -1);
|
|
1239
|
+
}
|
|
1240
|
+
return formattedHost;
|
|
1241
|
+
};
|
|
1242
|
+
var __defProp = Object.defineProperty;
|
|
1243
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1244
|
+
var __publicField = (obj, key, value) => {
|
|
1245
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1246
|
+
return value;
|
|
1247
|
+
};
|
|
1248
|
+
var Ollama$1 = class Ollama {
|
|
1249
|
+
constructor(config) {
|
|
1250
|
+
__publicField(this, "config");
|
|
1251
|
+
__publicField(this, "fetch");
|
|
1252
|
+
__publicField(this, "ongoingStreamedRequests", []);
|
|
1253
|
+
this.config = {
|
|
1254
|
+
host: "",
|
|
1255
|
+
headers: config?.headers
|
|
1256
|
+
};
|
|
1257
|
+
if (!config?.proxy) {
|
|
1258
|
+
this.config.host = formatHost(config?.host ?? defaultHost);
|
|
1259
|
+
}
|
|
1260
|
+
this.fetch = config?.fetch ?? fetch;
|
|
1261
|
+
}
|
|
1262
|
+
// Abort any ongoing streamed requests to Ollama
|
|
1263
|
+
abort() {
|
|
1264
|
+
for (const request of this.ongoingStreamedRequests) {
|
|
1265
|
+
request.abort();
|
|
1266
|
+
}
|
|
1267
|
+
this.ongoingStreamedRequests.length = 0;
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Processes a request to the Ollama server. If the request is streamable, it will return a
|
|
1271
|
+
* AbortableAsyncIterator that yields the response messages. Otherwise, it will return the response
|
|
1272
|
+
* object.
|
|
1273
|
+
* @param endpoint {string} - The endpoint to send the request to.
|
|
1274
|
+
* @param request {object} - The request object to send to the endpoint.
|
|
1275
|
+
* @protected {T | AbortableAsyncIterator<T>} - The response object or a AbortableAsyncIterator that yields
|
|
1276
|
+
* response messages.
|
|
1277
|
+
* @throws {Error} - If the response body is missing or if the response is an error.
|
|
1278
|
+
* @returns {Promise<T | AbortableAsyncIterator<T>>} - The response object or a AbortableAsyncIterator that yields the streamed response.
|
|
1279
|
+
*/
|
|
1280
|
+
async processStreamableRequest(endpoint, request) {
|
|
1281
|
+
request.stream = request.stream ?? false;
|
|
1282
|
+
const host = `${this.config.host}/api/${endpoint}`;
|
|
1283
|
+
if (request.stream) {
|
|
1284
|
+
const abortController = new AbortController();
|
|
1285
|
+
const response2 = await post(this.fetch, host, request, {
|
|
1286
|
+
signal: abortController.signal,
|
|
1287
|
+
headers: this.config.headers
|
|
1288
|
+
});
|
|
1289
|
+
if (!response2.body) {
|
|
1290
|
+
throw new Error("Missing body");
|
|
1291
|
+
}
|
|
1292
|
+
const itr = parseJSON(response2.body);
|
|
1293
|
+
const abortableAsyncIterator = new AbortableAsyncIterator(
|
|
1294
|
+
abortController,
|
|
1295
|
+
itr,
|
|
1296
|
+
() => {
|
|
1297
|
+
const i = this.ongoingStreamedRequests.indexOf(abortableAsyncIterator);
|
|
1298
|
+
if (i > -1) {
|
|
1299
|
+
this.ongoingStreamedRequests.splice(i, 1);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
);
|
|
1303
|
+
this.ongoingStreamedRequests.push(abortableAsyncIterator);
|
|
1304
|
+
return abortableAsyncIterator;
|
|
1305
|
+
}
|
|
1306
|
+
const response = await post(this.fetch, host, request, {
|
|
1307
|
+
headers: this.config.headers
|
|
1308
|
+
});
|
|
1309
|
+
return await response.json();
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Encodes an image to base64 if it is a Uint8Array.
|
|
1313
|
+
* @param image {Uint8Array | string} - The image to encode.
|
|
1314
|
+
* @returns {Promise<string>} - The base64 encoded image.
|
|
1315
|
+
*/
|
|
1316
|
+
async encodeImage(image) {
|
|
1317
|
+
if (typeof image !== "string") {
|
|
1318
|
+
const uint8Array = new Uint8Array(image);
|
|
1319
|
+
let byteString = "";
|
|
1320
|
+
const len = uint8Array.byteLength;
|
|
1321
|
+
for (let i = 0; i < len; i++) {
|
|
1322
|
+
byteString += String.fromCharCode(uint8Array[i]);
|
|
1323
|
+
}
|
|
1324
|
+
return btoa(byteString);
|
|
1325
|
+
}
|
|
1326
|
+
return image;
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Generates a response from a text prompt.
|
|
1330
|
+
* @param request {GenerateRequest} - The request object.
|
|
1331
|
+
* @returns {Promise<GenerateResponse | AbortableAsyncIterator<GenerateResponse>>} - The response object or
|
|
1332
|
+
* an AbortableAsyncIterator that yields response messages.
|
|
1333
|
+
*/
|
|
1334
|
+
async generate(request) {
|
|
1335
|
+
if (request.images) {
|
|
1336
|
+
request.images = await Promise.all(request.images.map(this.encodeImage.bind(this)));
|
|
1337
|
+
}
|
|
1338
|
+
return this.processStreamableRequest("generate", request);
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Chats with the model. The request object can contain messages with images that are either
|
|
1342
|
+
* Uint8Arrays or base64 encoded strings. The images will be base64 encoded before sending the
|
|
1343
|
+
* request.
|
|
1344
|
+
* @param request {ChatRequest} - The request object.
|
|
1345
|
+
* @returns {Promise<ChatResponse | AbortableAsyncIterator<ChatResponse>>} - The response object or an
|
|
1346
|
+
* AbortableAsyncIterator that yields response messages.
|
|
1347
|
+
*/
|
|
1348
|
+
async chat(request) {
|
|
1349
|
+
if (request.messages) {
|
|
1350
|
+
for (const message of request.messages) {
|
|
1351
|
+
if (message.images) {
|
|
1352
|
+
message.images = await Promise.all(
|
|
1353
|
+
message.images.map(this.encodeImage.bind(this))
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return this.processStreamableRequest("chat", request);
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Creates a new model from a stream of data.
|
|
1362
|
+
* @param request {CreateRequest} - The request object.
|
|
1363
|
+
* @returns {Promise<ProgressResponse | AbortableAsyncIterator<ProgressResponse>>} - The response object or a stream of progress responses.
|
|
1364
|
+
*/
|
|
1365
|
+
async create(request) {
|
|
1366
|
+
return this.processStreamableRequest("create", {
|
|
1367
|
+
...request
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Pulls a model from the Ollama registry. The request object can contain a stream flag to indicate if the
|
|
1372
|
+
* response should be streamed.
|
|
1373
|
+
* @param request {PullRequest} - The request object.
|
|
1374
|
+
* @returns {Promise<ProgressResponse | AbortableAsyncIterator<ProgressResponse>>} - The response object or
|
|
1375
|
+
* an AbortableAsyncIterator that yields response messages.
|
|
1376
|
+
*/
|
|
1377
|
+
async pull(request) {
|
|
1378
|
+
return this.processStreamableRequest("pull", {
|
|
1379
|
+
name: request.model,
|
|
1380
|
+
stream: request.stream,
|
|
1381
|
+
insecure: request.insecure
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Pushes a model to the Ollama registry. The request object can contain a stream flag to indicate if the
|
|
1386
|
+
* response should be streamed.
|
|
1387
|
+
* @param request {PushRequest} - The request object.
|
|
1388
|
+
* @returns {Promise<ProgressResponse | AbortableAsyncIterator<ProgressResponse>>} - The response object or
|
|
1389
|
+
* an AbortableAsyncIterator that yields response messages.
|
|
1390
|
+
*/
|
|
1391
|
+
async push(request) {
|
|
1392
|
+
return this.processStreamableRequest("push", {
|
|
1393
|
+
name: request.model,
|
|
1394
|
+
stream: request.stream,
|
|
1395
|
+
insecure: request.insecure
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Deletes a model from the server. The request object should contain the name of the model to
|
|
1400
|
+
* delete.
|
|
1401
|
+
* @param request {DeleteRequest} - The request object.
|
|
1402
|
+
* @returns {Promise<StatusResponse>} - The response object.
|
|
1403
|
+
*/
|
|
1404
|
+
async delete(request) {
|
|
1405
|
+
await del(
|
|
1406
|
+
this.fetch,
|
|
1407
|
+
`${this.config.host}/api/delete`,
|
|
1408
|
+
{ name: request.model },
|
|
1409
|
+
{ headers: this.config.headers }
|
|
1410
|
+
);
|
|
1411
|
+
return { status: "success" };
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Copies a model from one name to another. The request object should contain the name of the
|
|
1415
|
+
* model to copy and the new name.
|
|
1416
|
+
* @param request {CopyRequest} - The request object.
|
|
1417
|
+
* @returns {Promise<StatusResponse>} - The response object.
|
|
1418
|
+
*/
|
|
1419
|
+
async copy(request) {
|
|
1420
|
+
await post(this.fetch, `${this.config.host}/api/copy`, { ...request }, {
|
|
1421
|
+
headers: this.config.headers
|
|
1422
|
+
});
|
|
1423
|
+
return { status: "success" };
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Lists the models on the server.
|
|
1427
|
+
* @returns {Promise<ListResponse>} - The response object.
|
|
1428
|
+
* @throws {Error} - If the response body is missing.
|
|
1429
|
+
*/
|
|
1430
|
+
async list() {
|
|
1431
|
+
const response = await get(this.fetch, `${this.config.host}/api/tags`, {
|
|
1432
|
+
headers: this.config.headers
|
|
1433
|
+
});
|
|
1434
|
+
return await response.json();
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Shows the metadata of a model. The request object should contain the name of the model.
|
|
1438
|
+
* @param request {ShowRequest} - The request object.
|
|
1439
|
+
* @returns {Promise<ShowResponse>} - The response object.
|
|
1440
|
+
*/
|
|
1441
|
+
async show(request) {
|
|
1442
|
+
const response = await post(this.fetch, `${this.config.host}/api/show`, {
|
|
1443
|
+
...request
|
|
1444
|
+
}, {
|
|
1445
|
+
headers: this.config.headers
|
|
1446
|
+
});
|
|
1447
|
+
return await response.json();
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Embeds text input into vectors.
|
|
1451
|
+
* @param request {EmbedRequest} - The request object.
|
|
1452
|
+
* @returns {Promise<EmbedResponse>} - The response object.
|
|
1453
|
+
*/
|
|
1454
|
+
async embed(request) {
|
|
1455
|
+
const response = await post(this.fetch, `${this.config.host}/api/embed`, {
|
|
1456
|
+
...request
|
|
1457
|
+
}, {
|
|
1458
|
+
headers: this.config.headers
|
|
1459
|
+
});
|
|
1460
|
+
return await response.json();
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Embeds a text prompt into a vector.
|
|
1464
|
+
* @param request {EmbeddingsRequest} - The request object.
|
|
1465
|
+
* @returns {Promise<EmbeddingsResponse>} - The response object.
|
|
1466
|
+
*/
|
|
1467
|
+
async embeddings(request) {
|
|
1468
|
+
const response = await post(this.fetch, `${this.config.host}/api/embeddings`, {
|
|
1469
|
+
...request
|
|
1470
|
+
}, {
|
|
1471
|
+
headers: this.config.headers
|
|
1472
|
+
});
|
|
1473
|
+
return await response.json();
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Lists the running models on the server
|
|
1477
|
+
* @returns {Promise<ListResponse>} - The response object.
|
|
1478
|
+
* @throws {Error} - If the response body is missing.
|
|
1479
|
+
*/
|
|
1480
|
+
async ps() {
|
|
1481
|
+
const response = await get(this.fetch, `${this.config.host}/api/ps`, {
|
|
1482
|
+
headers: this.config.headers
|
|
1483
|
+
});
|
|
1484
|
+
return await response.json();
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Returns the Ollama server version.
|
|
1488
|
+
* @returns {Promise<VersionResponse>} - The server version object.
|
|
1489
|
+
*/
|
|
1490
|
+
async version() {
|
|
1491
|
+
const response = await get(this.fetch, `${this.config.host}/api/version`, {
|
|
1492
|
+
headers: this.config.headers
|
|
1493
|
+
});
|
|
1494
|
+
return await response.json();
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Performs web search using the Ollama web search API
|
|
1498
|
+
* @param request {WebSearchRequest} - The search request containing query and options
|
|
1499
|
+
* @returns {Promise<WebSearchResponse>} - The search results
|
|
1500
|
+
* @throws {Error} - If the request is invalid or the server returns an error
|
|
1501
|
+
*/
|
|
1502
|
+
async webSearch(request) {
|
|
1503
|
+
if (!request.query || request.query.length === 0) {
|
|
1504
|
+
throw new Error("Query is required");
|
|
1505
|
+
}
|
|
1506
|
+
const response = await post(this.fetch, `https://ollama.com/api/web_search`, { ...request }, {
|
|
1507
|
+
headers: this.config.headers
|
|
1508
|
+
});
|
|
1509
|
+
return await response.json();
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Fetches a single page using the Ollama web fetch API
|
|
1513
|
+
* @param request {WebFetchRequest} - The fetch request containing a URL
|
|
1514
|
+
* @returns {Promise<WebFetchResponse>} - The fetch result
|
|
1515
|
+
* @throws {Error} - If the request is invalid or the server returns an error
|
|
1516
|
+
*/
|
|
1517
|
+
async webFetch(request) {
|
|
1518
|
+
if (!request.url || request.url.length === 0) {
|
|
1519
|
+
throw new Error("URL is required");
|
|
1520
|
+
}
|
|
1521
|
+
const response = await post(this.fetch, `https://ollama.com/api/web_fetch`, { ...request }, { headers: this.config.headers });
|
|
1522
|
+
return await response.json();
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
var browser = new Ollama$1();
|
|
1526
|
+
|
|
1527
|
+
// ../../node_modules/.pnpm/ollama@0.6.3/node_modules/ollama/dist/index.mjs
|
|
1528
|
+
var Ollama2 = class extends Ollama$1 {
|
|
1529
|
+
async encodeImage(image) {
|
|
1530
|
+
if (typeof image !== "string") {
|
|
1531
|
+
return Buffer.from(image).toString("base64");
|
|
1532
|
+
}
|
|
1533
|
+
try {
|
|
1534
|
+
if (fs.existsSync(image)) {
|
|
1535
|
+
const fileBuffer = await promises.readFile(resolve(image));
|
|
1536
|
+
return Buffer.from(fileBuffer).toString("base64");
|
|
1537
|
+
}
|
|
1538
|
+
} catch {
|
|
1539
|
+
}
|
|
1540
|
+
return image;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* checks if a file exists
|
|
1544
|
+
* @param path {string} - The path to the file
|
|
1545
|
+
* @private @internal
|
|
1546
|
+
* @returns {Promise<boolean>} - Whether the file exists or not
|
|
1547
|
+
*/
|
|
1548
|
+
async fileExists(path) {
|
|
1549
|
+
try {
|
|
1550
|
+
await promises.access(path);
|
|
1551
|
+
return true;
|
|
1552
|
+
} catch {
|
|
1553
|
+
return false;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
async create(request) {
|
|
1557
|
+
if (request.from && await this.fileExists(resolve(request.from))) {
|
|
1558
|
+
throw Error("Creating with a local path is not currently supported from ollama-js");
|
|
1559
|
+
}
|
|
1560
|
+
if (request.stream) {
|
|
1561
|
+
return super.create(request);
|
|
1562
|
+
} else {
|
|
1563
|
+
return super.create(request);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
var index = new Ollama2();
|
|
1568
|
+
|
|
1569
|
+
// src/scanner/vision-analyzer.ts
|
|
1570
|
+
var UILINT_DEFAULT_VISION_MODEL = "qwen3-vl:8b-instruct";
|
|
1571
|
+
function stripCodeFences(input) {
|
|
1572
|
+
const trimmed = (input ?? "").trim();
|
|
1573
|
+
if (!trimmed.startsWith("```")) return trimmed;
|
|
1574
|
+
const m = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
1575
|
+
return m ? m[1].trim() : trimmed;
|
|
1576
|
+
}
|
|
1577
|
+
function extractJsonObject(input) {
|
|
1578
|
+
const s = stripCodeFences(input).trim();
|
|
1579
|
+
if (!s) return s;
|
|
1580
|
+
if (s.startsWith("{") && s.endsWith("}")) return s;
|
|
1581
|
+
const start = s.indexOf("{");
|
|
1582
|
+
const end = s.lastIndexOf("}");
|
|
1583
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
1584
|
+
return s.slice(start, end + 1).trim();
|
|
1585
|
+
}
|
|
1586
|
+
return s;
|
|
1587
|
+
}
|
|
1588
|
+
var DEFAULT_BASE_URL = "http://localhost:11434";
|
|
1589
|
+
var DEFAULT_TIMEOUT = 12e4;
|
|
1590
|
+
var VisionAnalyzer = class {
|
|
1591
|
+
baseUrl;
|
|
1592
|
+
visionModel;
|
|
1593
|
+
timeout;
|
|
1594
|
+
instrumentation;
|
|
1595
|
+
client;
|
|
1596
|
+
constructor(options = {}) {
|
|
1597
|
+
this.baseUrl = options.baseUrl || DEFAULT_BASE_URL;
|
|
1598
|
+
this.visionModel = options.visionModel || UILINT_DEFAULT_VISION_MODEL;
|
|
1599
|
+
this.timeout = options.timeout || DEFAULT_TIMEOUT;
|
|
1600
|
+
this.instrumentation = options.instrumentation;
|
|
1601
|
+
this.client = new Ollama2({ host: this.baseUrl });
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Analyzes a screenshot with element manifest for UI consistency issues
|
|
1605
|
+
*/
|
|
1606
|
+
async analyzeScreenshot(imageBase64, manifest, options = {}) {
|
|
1607
|
+
const startTime = Date.now();
|
|
1608
|
+
const prompt = this.buildVisionPrompt(manifest, options);
|
|
1609
|
+
const spanResult = this.instrumentation?.onGenerationStart?.({
|
|
1610
|
+
name: "vision-analyze",
|
|
1611
|
+
model: this.visionModel,
|
|
1612
|
+
prompt,
|
|
1613
|
+
metadata: {
|
|
1614
|
+
manifestSize: manifest.length,
|
|
1615
|
+
hasStyleGuide: !!options.styleGuide
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
const span = spanResult || void 0;
|
|
1619
|
+
try {
|
|
1620
|
+
const rawResponse = options.onProgress ? await this.chatVisionStreaming(
|
|
1621
|
+
imageBase64,
|
|
1622
|
+
prompt,
|
|
1623
|
+
options.onProgress,
|
|
1624
|
+
span
|
|
1625
|
+
) : await this.chatVision(imageBase64, prompt, span);
|
|
1626
|
+
const issues = this.parseVisionResponse(rawResponse, manifest);
|
|
1627
|
+
return {
|
|
1628
|
+
issues,
|
|
1629
|
+
analysisTime: Date.now() - startTime,
|
|
1630
|
+
prompt,
|
|
1631
|
+
rawResponse
|
|
1632
|
+
};
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
const analysisTime = Date.now() - startTime;
|
|
1635
|
+
const err = error instanceof Error ? error : new Error(String(error ?? "Unknown error"));
|
|
1636
|
+
console.error("[VisionAnalyzer] Analysis failed", {
|
|
1637
|
+
baseUrl: this.baseUrl,
|
|
1638
|
+
model: this.visionModel,
|
|
1639
|
+
manifestSize: manifest.length,
|
|
1640
|
+
analysisTime,
|
|
1641
|
+
error: err.message
|
|
1642
|
+
});
|
|
1643
|
+
span?.end("", { error: err.message });
|
|
1644
|
+
throw err;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Builds the vision analysis prompt
|
|
1649
|
+
*/
|
|
1650
|
+
buildVisionPrompt(manifest, options) {
|
|
1651
|
+
const styleGuideSection = options.styleGuide ? `## Style Guide
|
|
1652
|
+
${options.styleGuide}
|
|
1653
|
+
|
|
1654
|
+
` : "";
|
|
1655
|
+
const additionalSection = options.additionalContext ? `## Additional Context
|
|
1656
|
+
${options.additionalContext}
|
|
1657
|
+
|
|
1658
|
+
` : "";
|
|
1659
|
+
const manifestSection = this.buildManifestSection(manifest);
|
|
1660
|
+
return `You are a UI consistency analyzer examining a screenshot of a web page.
|
|
1661
|
+
Analyze the visual appearance and identify any UI consistency issues.
|
|
1662
|
+
|
|
1663
|
+
${styleGuideSection}${additionalSection}${manifestSection}
|
|
1664
|
+
|
|
1665
|
+
## Task
|
|
1666
|
+
|
|
1667
|
+
Examine the screenshot and identify visual consistency issues such as:
|
|
1668
|
+
1. **Spacing inconsistencies**: Elements with uneven padding, margins, or gaps
|
|
1669
|
+
2. **Alignment issues**: Elements that should be aligned but aren't
|
|
1670
|
+
3. **Color inconsistencies**: Similar colors that should be identical, or colors that don't match the style guide
|
|
1671
|
+
4. **Typography issues**: Inconsistent font sizes, weights, or families
|
|
1672
|
+
5. **Layout problems**: Broken layouts, overlapping elements, or visual hierarchy issues
|
|
1673
|
+
6. **Contrast issues**: Text that's hard to read or UI elements with insufficient contrast
|
|
1674
|
+
|
|
1675
|
+
For each issue found, reference the element by its visible text so we can map it back to the source code.
|
|
1676
|
+
|
|
1677
|
+
## Response Format
|
|
1678
|
+
|
|
1679
|
+
Respond with JSON ONLY. Return a single JSON object:
|
|
1680
|
+
|
|
1681
|
+
\`\`\`json
|
|
1682
|
+
{
|
|
1683
|
+
"issues": [
|
|
1684
|
+
{
|
|
1685
|
+
"elementText": "Submit Order",
|
|
1686
|
+
"message": "Button has inconsistent padding compared to other buttons (appears larger)",
|
|
1687
|
+
"category": "spacing",
|
|
1688
|
+
"severity": "warning"
|
|
1689
|
+
}
|
|
1690
|
+
]
|
|
1691
|
+
}
|
|
1692
|
+
\`\`\`
|
|
1693
|
+
|
|
1694
|
+
Categories: spacing, alignment, color, typography, layout, contrast, visual-hierarchy
|
|
1695
|
+
Severities: error (major issue), warning (should fix), info (minor/suggestion)
|
|
1696
|
+
|
|
1697
|
+
IMPORTANT:
|
|
1698
|
+
- Reference elements by their visible text content
|
|
1699
|
+
- Be specific about what's wrong and what the expected state should be
|
|
1700
|
+
- Only report significant visual inconsistencies
|
|
1701
|
+
- If no issues are found, return {"issues": []}`;
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Builds the manifest section of the prompt
|
|
1705
|
+
*/
|
|
1706
|
+
buildManifestSection(manifest) {
|
|
1707
|
+
if (manifest.length === 0) {
|
|
1708
|
+
return "";
|
|
1709
|
+
}
|
|
1710
|
+
const elements = manifest.map((el) => {
|
|
1711
|
+
const parts = [`- "${el.text}"`];
|
|
1712
|
+
if (el.role) parts.push(`(${el.role})`);
|
|
1713
|
+
if (el.instanceCount && el.instanceCount > 1) {
|
|
1714
|
+
parts.push(`[${el.instanceCount} instances]`);
|
|
1715
|
+
}
|
|
1716
|
+
return parts.join(" ");
|
|
1717
|
+
});
|
|
1718
|
+
return `## Page Elements
|
|
1719
|
+
|
|
1720
|
+
The following elements are visible on the page (reference by text when reporting issues):
|
|
1721
|
+
|
|
1722
|
+
${elements.join("\n")}
|
|
1723
|
+
|
|
1724
|
+
`;
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Non-streaming chat API call for vision
|
|
1728
|
+
*/
|
|
1729
|
+
async chatVision(imageBase64, prompt, span) {
|
|
1730
|
+
const controller = new AbortController();
|
|
1731
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1732
|
+
try {
|
|
1733
|
+
const base64Data = imageBase64.includes(",") ? imageBase64.split(",")[1] : imageBase64;
|
|
1734
|
+
const response = await this.client.chat({
|
|
1735
|
+
model: this.visionModel,
|
|
1736
|
+
messages: [
|
|
1737
|
+
{
|
|
1738
|
+
role: "user",
|
|
1739
|
+
content: prompt,
|
|
1740
|
+
images: [base64Data]
|
|
1741
|
+
}
|
|
1742
|
+
],
|
|
1743
|
+
format: "json",
|
|
1744
|
+
options: {
|
|
1745
|
+
// Increase context window for large prompts
|
|
1746
|
+
num_ctx: 8192
|
|
1747
|
+
}
|
|
1748
|
+
});
|
|
1749
|
+
const output = response.message?.content || "";
|
|
1750
|
+
if (!output.trim()) {
|
|
1751
|
+
const diag = JSON.stringify(
|
|
1752
|
+
{
|
|
1753
|
+
done_reason: response.done_reason,
|
|
1754
|
+
model: response.model,
|
|
1755
|
+
prompt_eval_count: response.prompt_eval_count,
|
|
1756
|
+
eval_count: response.eval_count
|
|
1757
|
+
},
|
|
1758
|
+
null,
|
|
1759
|
+
2
|
|
1760
|
+
);
|
|
1761
|
+
const errorMsg = `Vision model returned empty output. Diagnostics: ${diag}`;
|
|
1762
|
+
span?.end("", { error: errorMsg });
|
|
1763
|
+
throw new Error(errorMsg);
|
|
1764
|
+
}
|
|
1765
|
+
span?.end(output, {
|
|
1766
|
+
promptTokens: response.prompt_eval_count,
|
|
1767
|
+
completionTokens: response.eval_count,
|
|
1768
|
+
totalTokens: (response.prompt_eval_count || 0) + (response.eval_count || 0) || void 0
|
|
1769
|
+
});
|
|
1770
|
+
return output;
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
span?.end("", { error: String(error) });
|
|
1773
|
+
throw error;
|
|
1774
|
+
} finally {
|
|
1775
|
+
clearTimeout(timeoutId);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Streaming chat API call for vision
|
|
1780
|
+
* Uses ollama package's async iterator for streaming
|
|
1781
|
+
*/
|
|
1782
|
+
async chatVisionStreaming(imageBase64, prompt, onProgress, span) {
|
|
1783
|
+
const controller = new AbortController();
|
|
1784
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1785
|
+
let promptTokens;
|
|
1786
|
+
let completionTokens;
|
|
1787
|
+
let chunksReceived = 0;
|
|
1788
|
+
let lastProgressAt = Date.now();
|
|
1789
|
+
let emittedWaitingLine = false;
|
|
1790
|
+
try {
|
|
1791
|
+
const base64Data = imageBase64.includes(",") ? imageBase64.split(",")[1] : imageBase64;
|
|
1792
|
+
const stream = await this.client.chat({
|
|
1793
|
+
model: this.visionModel,
|
|
1794
|
+
messages: [
|
|
1795
|
+
{
|
|
1796
|
+
role: "user",
|
|
1797
|
+
content: prompt,
|
|
1798
|
+
images: [base64Data]
|
|
1799
|
+
}
|
|
1800
|
+
],
|
|
1801
|
+
// Enable thinking for streaming so we can surface reasoning traces for thinking-capable models.
|
|
1802
|
+
// Models that don't support it should ignore it.
|
|
1803
|
+
think: false,
|
|
1804
|
+
stream: true,
|
|
1805
|
+
format: "json",
|
|
1806
|
+
options: {
|
|
1807
|
+
num_ctx: 8192
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
let fullResponse = "";
|
|
1811
|
+
let lastLatestLine = "";
|
|
1812
|
+
for await (const chunk of stream) {
|
|
1813
|
+
chunksReceived++;
|
|
1814
|
+
if (!emittedWaitingLine && !fullResponse.trim()) {
|
|
1815
|
+
onProgress("(waiting for model output\u2026)", fullResponse);
|
|
1816
|
+
emittedWaitingLine = true;
|
|
1817
|
+
}
|
|
1818
|
+
const thinking = chunk?.message?.thinking || "";
|
|
1819
|
+
if (thinking) {
|
|
1820
|
+
onProgress(lastLatestLine, fullResponse, void 0, thinking);
|
|
1821
|
+
lastProgressAt = Date.now();
|
|
1822
|
+
}
|
|
1823
|
+
const content = chunk.message?.content || "";
|
|
1824
|
+
if (content) {
|
|
1825
|
+
fullResponse += content;
|
|
1826
|
+
const responseLines = fullResponse.split("\n");
|
|
1827
|
+
const latestLine = responseLines[responseLines.length - 1] || responseLines[responseLines.length - 2] || "";
|
|
1828
|
+
lastLatestLine = latestLine.trim();
|
|
1829
|
+
onProgress(lastLatestLine, fullResponse, content);
|
|
1830
|
+
lastProgressAt = Date.now();
|
|
1831
|
+
}
|
|
1832
|
+
if (chunk.done) {
|
|
1833
|
+
promptTokens = chunk.prompt_eval_count;
|
|
1834
|
+
completionTokens = chunk.eval_count;
|
|
1835
|
+
}
|
|
1836
|
+
if (!fullResponse.trim() && Date.now() - lastProgressAt > 4e3) {
|
|
1837
|
+
onProgress(
|
|
1838
|
+
`(streaming\u2026 received ${chunksReceived} chunks)`,
|
|
1839
|
+
fullResponse
|
|
1840
|
+
);
|
|
1841
|
+
lastProgressAt = Date.now();
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
if (!fullResponse.trim()) {
|
|
1845
|
+
const diag = JSON.stringify(
|
|
1846
|
+
{
|
|
1847
|
+
chunksReceived,
|
|
1848
|
+
promptTokens,
|
|
1849
|
+
completionTokens,
|
|
1850
|
+
model: this.visionModel
|
|
1851
|
+
},
|
|
1852
|
+
null,
|
|
1853
|
+
2
|
|
1854
|
+
);
|
|
1855
|
+
const errorMsg = `Vision model returned empty output (streaming). Diagnostics: ${diag}`;
|
|
1856
|
+
span?.end("", { error: errorMsg });
|
|
1857
|
+
throw new Error(errorMsg);
|
|
1858
|
+
}
|
|
1859
|
+
span?.end(fullResponse, {
|
|
1860
|
+
promptTokens,
|
|
1861
|
+
completionTokens,
|
|
1862
|
+
totalTokens: (promptTokens || 0) + (completionTokens || 0) || void 0
|
|
1863
|
+
});
|
|
1864
|
+
return fullResponse;
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
span?.end("", { error: String(error) });
|
|
1867
|
+
throw error;
|
|
1868
|
+
} finally {
|
|
1869
|
+
clearTimeout(timeoutId);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Parses the vision LLM response and matches elements to manifest
|
|
1874
|
+
*/
|
|
1875
|
+
parseVisionResponse(response, manifest) {
|
|
1876
|
+
try {
|
|
1877
|
+
const jsonText = extractJsonObject(response);
|
|
1878
|
+
const parsed = JSON.parse(jsonText);
|
|
1879
|
+
const rawIssues = parsed.issues || [];
|
|
1880
|
+
return rawIssues.map((issue) => {
|
|
1881
|
+
const matchedElement = this.matchElementByText(
|
|
1882
|
+
issue.elementText,
|
|
1883
|
+
manifest
|
|
1884
|
+
);
|
|
1885
|
+
return {
|
|
1886
|
+
...issue,
|
|
1887
|
+
dataLoc: matchedElement?.dataLoc
|
|
1888
|
+
};
|
|
1889
|
+
});
|
|
1890
|
+
} catch {
|
|
1891
|
+
const s = response ?? "";
|
|
1892
|
+
const previewHead = s ? s.slice(0, 600) : "";
|
|
1893
|
+
const previewTail = s && s.length > 600 ? s.slice(-400) : "";
|
|
1894
|
+
throw new Error(
|
|
1895
|
+
`Vision model returned non-JSON output (expected JSON). length=${s.length} head=${JSON.stringify(previewHead)} tail=${JSON.stringify(
|
|
1896
|
+
previewTail
|
|
1897
|
+
)}`
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Matches element text from LLM response to manifest entries
|
|
1903
|
+
*/
|
|
1904
|
+
matchElementByText(elementText, manifest) {
|
|
1905
|
+
if (!elementText) return void 0;
|
|
1906
|
+
const normalizedSearch = elementText.toLowerCase().trim();
|
|
1907
|
+
for (const entry of manifest) {
|
|
1908
|
+
if (entry.text.toLowerCase().trim() === normalizedSearch) {
|
|
1909
|
+
return entry;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
for (const entry of manifest) {
|
|
1913
|
+
const normalizedEntry = entry.text.toLowerCase().trim();
|
|
1914
|
+
if (normalizedEntry.includes(normalizedSearch) || normalizedSearch.includes(normalizedEntry)) {
|
|
1915
|
+
return entry;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return void 0;
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Checks if the vision model is available
|
|
1922
|
+
*/
|
|
1923
|
+
async isAvailable() {
|
|
1924
|
+
try {
|
|
1925
|
+
const models = await this.client.list();
|
|
1926
|
+
return models.models.some(
|
|
1927
|
+
(m) => m.name === this.visionModel || m.name.startsWith(this.visionModel.split(":")[0])
|
|
1928
|
+
);
|
|
1929
|
+
} catch {
|
|
1930
|
+
return false;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Gets the current vision model
|
|
1935
|
+
*/
|
|
1936
|
+
getModel() {
|
|
1937
|
+
return this.visionModel;
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Gets the Ollama base URL (for diagnostics)
|
|
1941
|
+
*/
|
|
1942
|
+
getBaseUrl() {
|
|
1943
|
+
return this.baseUrl;
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Sets the vision model
|
|
1947
|
+
*/
|
|
1948
|
+
setModel(model) {
|
|
1949
|
+
this.visionModel = model;
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Sets instrumentation callbacks
|
|
1953
|
+
*/
|
|
1954
|
+
setInstrumentation(instrumentation) {
|
|
1955
|
+
this.instrumentation = instrumentation;
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
var defaultAnalyzer = null;
|
|
1959
|
+
function getVisionAnalyzer(options) {
|
|
1960
|
+
if (!defaultAnalyzer || options) {
|
|
1961
|
+
defaultAnalyzer = new VisionAnalyzer(options);
|
|
1962
|
+
}
|
|
1963
|
+
return defaultAnalyzer;
|
|
1964
|
+
}
|
|
484
1965
|
export {
|
|
485
1966
|
OllamaClient,
|
|
486
1967
|
STYLEGUIDE_PATHS,
|
|
487
1968
|
UILINT_DEFAULT_OLLAMA_MODEL,
|
|
1969
|
+
UILINT_DEFAULT_VISION_MODEL,
|
|
1970
|
+
VisionAnalyzer,
|
|
488
1971
|
analyzeConsistency,
|
|
489
1972
|
buildAnalysisPrompt,
|
|
490
1973
|
buildConsistencyPrompt,
|
|
@@ -517,6 +2000,7 @@ export {
|
|
|
517
2000
|
generateStyleGuideFromStyles,
|
|
518
2001
|
getDefaultStyleGuidePath,
|
|
519
2002
|
getOllamaClient,
|
|
2003
|
+
getVisionAnalyzer,
|
|
520
2004
|
hasAnalyzableGroups,
|
|
521
2005
|
hasStdin,
|
|
522
2006
|
isJSON,
|