Project Files
dist / index.js
#!/usr/bin/env node
'use strict';
var sdk = require('@lmstudio/sdk');
var zod = require('zod');
var path = require('path');
var fs = require('fs');
var os = require('os');
require('crypto');
var child_process = require('child_process');
require('node:path');
var http = require('http');
require('node:fs');
var util = require('util');
var url = require('url');
async function readState$1(chatWd){const p=path.join(chatWd,"chat_media_state.json");try{const raw=await fs.promises.readFile(p,"utf-8");const json=JSON.parse(raw);return {attachments:Array.isArray(json?.attachments)?json.attachments:[],variants:Array.isArray(json?.variants)?json.variants:[],pictures:Array.isArray(json?.pictures)?json.pictures:[],images:Array.isArray(json?.images)?json.images:[],counters:json?.counters||{}}}catch{return {attachments:[],variants:[],pictures:[],images:[],counters:{}}}}
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
__defProp(target, "default", { value: mod, enumerable: true }) ,
mod
));
// node_modules/flatbuffers/js/constants.js
var require_constants = __commonJS({
"node_modules/flatbuffers/js/constants.js"(exports$1) {
Object.defineProperty(exports$1, "__esModule", { value: true });
exports$1.SIZE_PREFIX_LENGTH = exports$1.FILE_IDENTIFIER_LENGTH = exports$1.SIZEOF_INT = exports$1.SIZEOF_SHORT = void 0;
exports$1.SIZEOF_SHORT = 2;
exports$1.SIZEOF_INT = 4;
exports$1.FILE_IDENTIFIER_LENGTH = 4;
exports$1.SIZE_PREFIX_LENGTH = 4;
}
});
// node_modules/flatbuffers/js/utils.js
var require_utils = __commonJS({
"node_modules/flatbuffers/js/utils.js"(exports$1) {
Object.defineProperty(exports$1, "__esModule", { value: true });
exports$1.isLittleEndian = exports$1.float64 = exports$1.float32 = exports$1.int32 = void 0;
exports$1.int32 = new Int32Array(2);
exports$1.float32 = new Float32Array(exports$1.int32.buffer);
exports$1.float64 = new Float64Array(exports$1.int32.buffer);
exports$1.isLittleEndian = new Uint16Array(new Uint8Array([1, 0]).buffer)[0] === 1;
}
});
// node_modules/flatbuffers/js/encoding.js
var require_encoding = __commonJS({
"node_modules/flatbuffers/js/encoding.js"(exports$1) {
Object.defineProperty(exports$1, "__esModule", { value: true });
exports$1.Encoding = void 0;
var Encoding;
(function(Encoding2) {
Encoding2[Encoding2["UTF8_BYTES"] = 1] = "UTF8_BYTES";
Encoding2[Encoding2["UTF16_STRING"] = 2] = "UTF16_STRING";
})(Encoding || (exports$1.Encoding = Encoding = {}));
}
});
// node_modules/flatbuffers/js/byte-buffer.js
var require_byte_buffer = __commonJS({
"node_modules/flatbuffers/js/byte-buffer.js"(exports$1) {
Object.defineProperty(exports$1, "__esModule", { value: true });
exports$1.ByteBuffer = void 0;
var constants_js_1 = require_constants();
var encoding_js_1 = require_encoding();
var utils_js_1 = require_utils();
var ByteBuffer = class _ByteBuffer {
/**
* Create a new ByteBuffer with a given array of bytes (`Uint8Array`)
*/
constructor(bytes_) {
this.bytes_ = bytes_;
this.position_ = 0;
this.text_decoder_ = new TextDecoder();
}
/**
* Create and allocate a new ByteBuffer with a given size.
*/
static allocate(byte_size) {
return new _ByteBuffer(new Uint8Array(byte_size));
}
clear() {
this.position_ = 0;
}
/**
* Get the underlying `Uint8Array`.
*/
bytes() {
return this.bytes_;
}
/**
* Get the buffer's position.
*/
position() {
return this.position_;
}
/**
* Set the buffer's position.
*/
setPosition(position) {
this.position_ = position;
}
/**
* Get the buffer's capacity.
*/
capacity() {
return this.bytes_.length;
}
readInt8(offset) {
return this.readUint8(offset) << 24 >> 24;
}
readUint8(offset) {
return this.bytes_[offset];
}
readInt16(offset) {
return this.readUint16(offset) << 16 >> 16;
}
readUint16(offset) {
return this.bytes_[offset] | this.bytes_[offset + 1] << 8;
}
readInt32(offset) {
return this.bytes_[offset] | this.bytes_[offset + 1] << 8 | this.bytes_[offset + 2] << 16 | this.bytes_[offset + 3] << 24;
}
readUint32(offset) {
return this.readInt32(offset) >>> 0;
}
readInt64(offset) {
return BigInt.asIntN(64, BigInt(this.readUint32(offset)) + (BigInt(this.readUint32(offset + 4)) << BigInt(32)));
}
readUint64(offset) {
return BigInt.asUintN(64, BigInt(this.readUint32(offset)) + (BigInt(this.readUint32(offset + 4)) << BigInt(32)));
}
readFloat32(offset) {
utils_js_1.int32[0] = this.readInt32(offset);
return utils_js_1.float32[0];
}
readFloat64(offset) {
utils_js_1.int32[utils_js_1.isLittleEndian ? 0 : 1] = this.readInt32(offset);
utils_js_1.int32[utils_js_1.isLittleEndian ? 1 : 0] = this.readInt32(offset + 4);
return utils_js_1.float64[0];
}
writeInt8(offset, value) {
this.bytes_[offset] = value;
}
writeUint8(offset, value) {
this.bytes_[offset] = value;
}
writeInt16(offset, value) {
this.bytes_[offset] = value;
this.bytes_[offset + 1] = value >> 8;
}
writeUint16(offset, value) {
this.bytes_[offset] = value;
this.bytes_[offset + 1] = value >> 8;
}
writeInt32(offset, value) {
this.bytes_[offset] = value;
this.bytes_[offset + 1] = value >> 8;
this.bytes_[offset + 2] = value >> 16;
this.bytes_[offset + 3] = value >> 24;
}
writeUint32(offset, value) {
this.bytes_[offset] = value;
this.bytes_[offset + 1] = value >> 8;
this.bytes_[offset + 2] = value >> 16;
this.bytes_[offset + 3] = value >> 24;
}
writeInt64(offset, value) {
this.writeInt32(offset, Number(BigInt.asIntN(32, value)));
this.writeInt32(offset + 4, Number(BigInt.asIntN(32, value >> BigInt(32))));
}
writeUint64(offset, value) {
this.writeUint32(offset, Number(BigInt.asUintN(32, value)));
this.writeUint32(offset + 4, Number(BigInt.asUintN(32, value >> BigInt(32))));
}
writeFloat32(offset, value) {
utils_js_1.float32[0] = value;
this.writeInt32(offset, utils_js_1.int32[0]);
}
writeFloat64(offset, value) {
utils_js_1.float64[0] = value;
this.writeInt32(offset, utils_js_1.int32[utils_js_1.isLittleEndian ? 0 : 1]);
this.writeInt32(offset + 4, utils_js_1.int32[utils_js_1.isLittleEndian ? 1 : 0]);
}
/**
* Return the file identifier. Behavior is undefined for FlatBuffers whose
* schema does not include a file_identifier (likely points at padding or the
* start of a the root vtable).
*/
getBufferIdentifier() {
if (this.bytes_.length < this.position_ + constants_js_1.SIZEOF_INT + constants_js_1.FILE_IDENTIFIER_LENGTH) {
throw new Error("FlatBuffers: ByteBuffer is too short to contain an identifier.");
}
let result = "";
for (let i = 0; i < constants_js_1.FILE_IDENTIFIER_LENGTH; i++) {
result += String.fromCharCode(this.readInt8(this.position_ + constants_js_1.SIZEOF_INT + i));
}
return result;
}
/**
* Look up a field in the vtable, return an offset into the object, or 0 if the
* field is not present.
*/
__offset(bb_pos, vtable_offset) {
const vtable = bb_pos - this.readInt32(bb_pos);
return vtable_offset < this.readInt16(vtable) ? this.readInt16(vtable + vtable_offset) : 0;
}
/**
* Initialize any Table-derived type to point to the union at the given offset.
*/
__union(t, offset) {
t.bb_pos = offset + this.readInt32(offset);
t.bb = this;
return t;
}
/**
* Create a JavaScript string from UTF-8 data stored inside the FlatBuffer.
* This allocates a new string and converts to wide chars upon each access.
*
* To avoid the conversion to string, pass Encoding.UTF8_BYTES as the
* "optionalEncoding" argument. This is useful for avoiding conversion when
* the data will just be packaged back up in another FlatBuffer later on.
*
* @param offset
* @param opt_encoding Defaults to UTF16_STRING
*/
__string(offset, opt_encoding) {
offset += this.readInt32(offset);
const length = this.readInt32(offset);
offset += constants_js_1.SIZEOF_INT;
const utf8bytes = this.bytes_.subarray(offset, offset + length);
if (opt_encoding === encoding_js_1.Encoding.UTF8_BYTES)
return utf8bytes;
else
return this.text_decoder_.decode(utf8bytes);
}
/**
* Handle unions that can contain string as its member, if a Table-derived type then initialize it,
* if a string then return a new one
*
* WARNING: strings are immutable in JS so we can't change the string that the user gave us, this
* makes the behaviour of __union_with_string different compared to __union
*/
__union_with_string(o, offset) {
if (typeof o === "string") {
return this.__string(offset);
}
return this.__union(o, offset);
}
/**
* Retrieve the relative offset stored at "offset"
*/
__indirect(offset) {
return offset + this.readInt32(offset);
}
/**
* Get the start of data of a vector whose offset is stored at "offset" in this object.
*/
__vector(offset) {
return offset + this.readInt32(offset) + constants_js_1.SIZEOF_INT;
}
/**
* Get the length of a vector whose offset is stored at "offset" in this object.
*/
__vector_len(offset) {
return this.readInt32(offset + this.readInt32(offset));
}
__has_identifier(ident) {
if (ident.length != constants_js_1.FILE_IDENTIFIER_LENGTH) {
throw new Error("FlatBuffers: file identifier must be length " + constants_js_1.FILE_IDENTIFIER_LENGTH);
}
for (let i = 0; i < constants_js_1.FILE_IDENTIFIER_LENGTH; i++) {
if (ident.charCodeAt(i) != this.readInt8(this.position() + constants_js_1.SIZEOF_INT + i)) {
return false;
}
}
return true;
}
/**
* A helper function for generating list for obj api
*/
createScalarList(listAccessor, listLength) {
const ret = [];
for (let i = 0; i < listLength; ++i) {
const val = listAccessor(i);
if (val !== null) {
ret.push(val);
}
}
return ret;
}
/**
* A helper function for generating list for obj api
* @param listAccessor function that accepts an index and return data at that index
* @param listLength listLength
* @param res result list
*/
createObjList(listAccessor, listLength) {
const ret = [];
for (let i = 0; i < listLength; ++i) {
const val = listAccessor(i);
if (val !== null) {
ret.push(val.unpack());
}
}
return ret;
}
};
exports$1.ByteBuffer = ByteBuffer;
}
});
// node_modules/flatbuffers/js/builder.js
var require_builder = __commonJS({
"node_modules/flatbuffers/js/builder.js"(exports$1) {
Object.defineProperty(exports$1, "__esModule", { value: true });
exports$1.Builder = void 0;
var byte_buffer_js_1 = require_byte_buffer();
var constants_js_1 = require_constants();
var Builder = class _Builder {
/**
* Create a FlatBufferBuilder.
*/
constructor(opt_initial_size) {
this.minalign = 1;
this.vtable = null;
this.vtable_in_use = 0;
this.isNested = false;
this.object_start = 0;
this.vtables = [];
this.vector_num_elems = 0;
this.force_defaults = false;
this.string_maps = null;
this.text_encoder = new TextEncoder();
let initial_size;
if (!opt_initial_size) {
initial_size = 1024;
} else {
initial_size = opt_initial_size;
}
this.bb = byte_buffer_js_1.ByteBuffer.allocate(initial_size);
this.space = initial_size;
}
clear() {
this.bb.clear();
this.space = this.bb.capacity();
this.minalign = 1;
this.vtable = null;
this.vtable_in_use = 0;
this.isNested = false;
this.object_start = 0;
this.vtables = [];
this.vector_num_elems = 0;
this.force_defaults = false;
this.string_maps = null;
}
/**
* In order to save space, fields that are set to their default value
* don't get serialized into the buffer. Forcing defaults provides a
* way to manually disable this optimization.
*
* @param forceDefaults true always serializes default values
*/
forceDefaults(forceDefaults) {
this.force_defaults = forceDefaults;
}
/**
* Get the ByteBuffer representing the FlatBuffer. Only call this after you've
* called finish(). The actual data starts at the ByteBuffer's current position,
* not necessarily at 0.
*/
dataBuffer() {
return this.bb;
}
/**
* Get the bytes representing the FlatBuffer. Only call this after you've
* called finish().
*/
asUint8Array() {
return this.bb.bytes().subarray(this.bb.position(), this.bb.position() + this.offset());
}
/**
* Prepare to write an element of `size` after `additional_bytes` have been
* written, e.g. if you write a string, you need to align such the int length
* field is aligned to 4 bytes, and the string data follows it directly. If all
* you need to do is alignment, `additional_bytes` will be 0.
*
* @param size This is the of the new element to write
* @param additional_bytes The padding size
*/
prep(size, additional_bytes) {
if (size > this.minalign) {
this.minalign = size;
}
const align_size = ~(this.bb.capacity() - this.space + additional_bytes) + 1 & size - 1;
while (this.space < align_size + size + additional_bytes) {
const old_buf_size = this.bb.capacity();
this.bb = _Builder.growByteBuffer(this.bb);
this.space += this.bb.capacity() - old_buf_size;
}
this.pad(align_size);
}
pad(byte_size) {
for (let i = 0; i < byte_size; i++) {
this.bb.writeInt8(--this.space, 0);
}
}
writeInt8(value) {
this.bb.writeInt8(this.space -= 1, value);
}
writeInt16(value) {
this.bb.writeInt16(this.space -= 2, value);
}
writeInt32(value) {
this.bb.writeInt32(this.space -= 4, value);
}
writeInt64(value) {
this.bb.writeInt64(this.space -= 8, value);
}
writeFloat32(value) {
this.bb.writeFloat32(this.space -= 4, value);
}
writeFloat64(value) {
this.bb.writeFloat64(this.space -= 8, value);
}
/**
* Add an `int8` to the buffer, properly aligned, and grows the buffer (if necessary).
* @param value The `int8` to add the buffer.
*/
addInt8(value) {
this.prep(1, 0);
this.writeInt8(value);
}
/**
* Add an `int16` to the buffer, properly aligned, and grows the buffer (if necessary).
* @param value The `int16` to add the buffer.
*/
addInt16(value) {
this.prep(2, 0);
this.writeInt16(value);
}
/**
* Add an `int32` to the buffer, properly aligned, and grows the buffer (if necessary).
* @param value The `int32` to add the buffer.
*/
addInt32(value) {
this.prep(4, 0);
this.writeInt32(value);
}
/**
* Add an `int64` to the buffer, properly aligned, and grows the buffer (if necessary).
* @param value The `int64` to add the buffer.
*/
addInt64(value) {
this.prep(8, 0);
this.writeInt64(value);
}
/**
* Add a `float32` to the buffer, properly aligned, and grows the buffer (if necessary).
* @param value The `float32` to add the buffer.
*/
addFloat32(value) {
this.prep(4, 0);
this.writeFloat32(value);
}
/**
* Add a `float64` to the buffer, properly aligned, and grows the buffer (if necessary).
* @param value The `float64` to add the buffer.
*/
addFloat64(value) {
this.prep(8, 0);
this.writeFloat64(value);
}
addFieldInt8(voffset, value, defaultValue) {
if (this.force_defaults || value != defaultValue) {
this.addInt8(value);
this.slot(voffset);
}
}
addFieldInt16(voffset, value, defaultValue) {
if (this.force_defaults || value != defaultValue) {
this.addInt16(value);
this.slot(voffset);
}
}
addFieldInt32(voffset, value, defaultValue) {
if (this.force_defaults || value != defaultValue) {
this.addInt32(value);
this.slot(voffset);
}
}
addFieldInt64(voffset, value, defaultValue) {
if (this.force_defaults || value !== defaultValue) {
this.addInt64(value);
this.slot(voffset);
}
}
addFieldFloat32(voffset, value, defaultValue) {
if (this.force_defaults || value != defaultValue) {
this.addFloat32(value);
this.slot(voffset);
}
}
addFieldFloat64(voffset, value, defaultValue) {
if (this.force_defaults || value != defaultValue) {
this.addFloat64(value);
this.slot(voffset);
}
}
addFieldOffset(voffset, value, defaultValue) {
if (this.force_defaults || value != defaultValue) {
this.addOffset(value);
this.slot(voffset);
}
}
/**
* Structs are stored inline, so nothing additional is being added. `d` is always 0.
*/
addFieldStruct(voffset, value, defaultValue) {
if (value != defaultValue) {
this.nested(value);
this.slot(voffset);
}
}
/**
* Structures are always stored inline, they need to be created right
* where they're used. You'll get this assertion failure if you
* created it elsewhere.
*/
nested(obj) {
if (obj != this.offset()) {
throw new TypeError("FlatBuffers: struct must be serialized inline.");
}
}
/**
* Should not be creating any other object, string or vector
* while an object is being constructed
*/
notNested() {
if (this.isNested) {
throw new TypeError("FlatBuffers: object serialization must not be nested.");
}
}
/**
* Set the current vtable at `voffset` to the current location in the buffer.
*/
slot(voffset) {
if (this.vtable !== null)
this.vtable[voffset] = this.offset();
}
/**
* @returns Offset relative to the end of the buffer.
*/
offset() {
return this.bb.capacity() - this.space;
}
/**
* Doubles the size of the backing ByteBuffer and copies the old data towards
* the end of the new buffer (since we build the buffer backwards).
*
* @param bb The current buffer with the existing data
* @returns A new byte buffer with the old data copied
* to it. The data is located at the end of the buffer.
*
* uint8Array.set() formally takes {Array<number>|ArrayBufferView}, so to pass
* it a uint8Array we need to suppress the type check:
* @suppress {checkTypes}
*/
static growByteBuffer(bb) {
const old_buf_size = bb.capacity();
if (old_buf_size & 3221225472) {
throw new Error("FlatBuffers: cannot grow buffer beyond 2 gigabytes.");
}
const new_buf_size = old_buf_size << 1;
const nbb = byte_buffer_js_1.ByteBuffer.allocate(new_buf_size);
nbb.setPosition(new_buf_size - old_buf_size);
nbb.bytes().set(bb.bytes(), new_buf_size - old_buf_size);
return nbb;
}
/**
* Adds on offset, relative to where it will be written.
*
* @param offset The offset to add.
*/
addOffset(offset) {
this.prep(constants_js_1.SIZEOF_INT, 0);
this.writeInt32(this.offset() - offset + constants_js_1.SIZEOF_INT);
}
/**
* Start encoding a new object in the buffer. Users will not usually need to
* call this directly. The FlatBuffers compiler will generate helper methods
* that call this method internally.
*/
startObject(numfields) {
this.notNested();
if (this.vtable == null) {
this.vtable = [];
}
this.vtable_in_use = numfields;
for (let i = 0; i < numfields; i++) {
this.vtable[i] = 0;
}
this.isNested = true;
this.object_start = this.offset();
}
/**
* Finish off writing the object that is under construction.
*
* @returns The offset to the object inside `dataBuffer`
*/
endObject() {
if (this.vtable == null || !this.isNested) {
throw new Error("FlatBuffers: endObject called without startObject");
}
this.addInt32(0);
const vtableloc = this.offset();
let i = this.vtable_in_use - 1;
for (; i >= 0 && this.vtable[i] == 0; i--) {
}
const trimmed_size = i + 1;
for (; i >= 0; i--) {
this.addInt16(this.vtable[i] != 0 ? vtableloc - this.vtable[i] : 0);
}
const standard_fields = 2;
this.addInt16(vtableloc - this.object_start);
const len = (trimmed_size + standard_fields) * constants_js_1.SIZEOF_SHORT;
this.addInt16(len);
let existing_vtable = 0;
const vt1 = this.space;
outer_loop: for (i = 0; i < this.vtables.length; i++) {
const vt2 = this.bb.capacity() - this.vtables[i];
if (len == this.bb.readInt16(vt2)) {
for (let j = constants_js_1.SIZEOF_SHORT; j < len; j += constants_js_1.SIZEOF_SHORT) {
if (this.bb.readInt16(vt1 + j) != this.bb.readInt16(vt2 + j)) {
continue outer_loop;
}
}
existing_vtable = this.vtables[i];
break;
}
}
if (existing_vtable) {
this.space = this.bb.capacity() - vtableloc;
this.bb.writeInt32(this.space, existing_vtable - vtableloc);
} else {
this.vtables.push(this.offset());
this.bb.writeInt32(this.bb.capacity() - vtableloc, this.offset() - vtableloc);
}
this.isNested = false;
return vtableloc;
}
/**
* Finalize a buffer, poiting to the given `root_table`.
*/
finish(root_table, opt_file_identifier, opt_size_prefix) {
const size_prefix = opt_size_prefix ? constants_js_1.SIZE_PREFIX_LENGTH : 0;
if (opt_file_identifier) {
const file_identifier = opt_file_identifier;
this.prep(this.minalign, constants_js_1.SIZEOF_INT + constants_js_1.FILE_IDENTIFIER_LENGTH + size_prefix);
if (file_identifier.length != constants_js_1.FILE_IDENTIFIER_LENGTH) {
throw new TypeError("FlatBuffers: file identifier must be length " + constants_js_1.FILE_IDENTIFIER_LENGTH);
}
for (let i = constants_js_1.FILE_IDENTIFIER_LENGTH - 1; i >= 0; i--) {
this.writeInt8(file_identifier.charCodeAt(i));
}
}
this.prep(this.minalign, constants_js_1.SIZEOF_INT + size_prefix);
this.addOffset(root_table);
if (size_prefix) {
this.addInt32(this.bb.capacity() - this.space);
}
this.bb.setPosition(this.space);
}
/**
* Finalize a size prefixed buffer, pointing to the given `root_table`.
*/
finishSizePrefixed(root_table, opt_file_identifier) {
this.finish(root_table, opt_file_identifier, true);
}
/**
* This checks a required field has been set in a given table that has
* just been constructed.
*/
requiredField(table, field) {
const table_start = this.bb.capacity() - table;
const vtable_start = table_start - this.bb.readInt32(table_start);
const ok = field < this.bb.readInt16(vtable_start) && this.bb.readInt16(vtable_start + field) != 0;
if (!ok) {
throw new TypeError("FlatBuffers: field " + field + " must be set");
}
}
/**
* Start a new array/vector of objects. Users usually will not call
* this directly. The FlatBuffers compiler will create a start/end
* method for vector types in generated code.
*
* @param elem_size The size of each element in the array
* @param num_elems The number of elements in the array
* @param alignment The alignment of the array
*/
startVector(elem_size, num_elems, alignment) {
this.notNested();
this.vector_num_elems = num_elems;
this.prep(constants_js_1.SIZEOF_INT, elem_size * num_elems);
this.prep(alignment, elem_size * num_elems);
}
/**
* Finish off the creation of an array and all its elements. The array must be
* created with `startVector`.
*
* @returns The offset at which the newly created array
* starts.
*/
endVector() {
this.writeInt32(this.vector_num_elems);
return this.offset();
}
/**
* Encode the string `s` in the buffer using UTF-8. If the string passed has
* already been seen, we return the offset of the already written string
*
* @param s The string to encode
* @return The offset in the buffer where the encoded string starts
*/
createSharedString(s) {
if (!s) {
return 0;
}
if (!this.string_maps) {
this.string_maps = /* @__PURE__ */ new Map();
}
if (this.string_maps.has(s)) {
return this.string_maps.get(s);
}
const offset = this.createString(s);
this.string_maps.set(s, offset);
return offset;
}
/**
* Encode the string `s` in the buffer using UTF-8. If a Uint8Array is passed
* instead of a string, it is assumed to contain valid UTF-8 encoded data.
*
* @param s The string to encode
* @return The offset in the buffer where the encoded string starts
*/
createString(s) {
if (s === null || s === void 0) {
return 0;
}
let utf8;
if (s instanceof Uint8Array) {
utf8 = s;
} else {
utf8 = this.text_encoder.encode(s);
}
this.addInt8(0);
this.startVector(1, utf8.length, 1);
this.bb.setPosition(this.space -= utf8.length);
this.bb.bytes().set(utf8, this.space);
return this.endVector();
}
/**
* Create a byte vector.
*
* @param v The bytes to add
* @returns The offset in the buffer where the byte vector starts
*/
createByteVector(v) {
if (v === null || v === void 0) {
return 0;
}
this.startVector(1, v.length, 1);
this.bb.setPosition(this.space -= v.length);
this.bb.bytes().set(v, this.space);
return this.endVector();
}
/**
* A helper function to pack an object
*
* @returns offset of obj
*/
createObjectOffset(obj) {
if (obj === null) {
return 0;
}
if (typeof obj === "string") {
return this.createString(obj);
} else {
return obj.pack(this);
}
}
/**
* A helper function to pack a list of object
*
* @returns list of offsets of each non null object
*/
createObjectOffsetList(list) {
const ret = [];
for (let i = 0; i < list.length; ++i) {
const val = list[i];
if (val !== null) {
ret.push(this.createObjectOffset(val));
} else {
throw new TypeError("FlatBuffers: Argument for createObjectOffsetList cannot contain null.");
}
}
return ret;
}
createStructOffsetList(list, startFunc) {
startFunc(this, list.length);
this.createObjectOffsetList(list.slice().reverse());
return this.endVector();
}
};
exports$1.Builder = Builder;
}
});
// node_modules/flatbuffers/js/flatbuffers.js
var require_flatbuffers = __commonJS({
"node_modules/flatbuffers/js/flatbuffers.js"(exports$1) {
Object.defineProperty(exports$1, "__esModule", { value: true });
exports$1.Encoding = exports$1.ByteBuffer = exports$1.Builder = exports$1.isLittleEndian = exports$1.int32 = exports$1.float64 = exports$1.float32 = exports$1.SIZE_PREFIX_LENGTH = exports$1.SIZEOF_SHORT = exports$1.SIZEOF_INT = exports$1.FILE_IDENTIFIER_LENGTH = void 0;
var constants_js_1 = require_constants();
Object.defineProperty(exports$1, "FILE_IDENTIFIER_LENGTH", { enumerable: true, get: function() {
return constants_js_1.FILE_IDENTIFIER_LENGTH;
} });
Object.defineProperty(exports$1, "SIZEOF_INT", { enumerable: true, get: function() {
return constants_js_1.SIZEOF_INT;
} });
Object.defineProperty(exports$1, "SIZEOF_SHORT", { enumerable: true, get: function() {
return constants_js_1.SIZEOF_SHORT;
} });
Object.defineProperty(exports$1, "SIZE_PREFIX_LENGTH", { enumerable: true, get: function() {
return constants_js_1.SIZE_PREFIX_LENGTH;
} });
var utils_js_1 = require_utils();
Object.defineProperty(exports$1, "float32", { enumerable: true, get: function() {
return utils_js_1.float32;
} });
Object.defineProperty(exports$1, "float64", { enumerable: true, get: function() {
return utils_js_1.float64;
} });
Object.defineProperty(exports$1, "int32", { enumerable: true, get: function() {
return utils_js_1.int32;
} });
Object.defineProperty(exports$1, "isLittleEndian", { enumerable: true, get: function() {
return utils_js_1.isLittleEndian;
} });
var builder_js_1 = require_builder();
Object.defineProperty(exports$1, "Builder", { enumerable: true, get: function() {
return builder_js_1.Builder;
} });
var byte_buffer_js_1 = require_byte_buffer();
Object.defineProperty(exports$1, "ByteBuffer", { enumerable: true, get: function() {
return byte_buffer_js_1.ByteBuffer;
} });
var encoding_js_1 = require_encoding();
Object.defineProperty(exports$1, "Encoding", { enumerable: true, get: function() {
return encoding_js_1.Encoding;
} });
}
});
function resolveProjectRootFrom(startDir) {
try {
{
let dir = startDir;
for (let i = 0; i < 50; i++) {
try {
if (fs.existsSync(path.join(dir, "manifest.json"))) return dir;
} catch {
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
}
{
let dir = startDir;
for (let i = 0; i < 50; i++) {
try {
if (fs.existsSync(path.join(dir, "package.json"))) return dir;
} catch {
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
}
{
let dir = startDir;
for (let i = 0; i < 30; i++) {
const base = path.basename(dir);
if (base === "dist" || base === "src") return path.dirname(dir);
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
}
} catch {
}
return startDir;
}
function getProjectRoot() {
try {
if (fs.existsSync(path.join(process.cwd(), "manifest.json"))) {
return process.cwd();
}
} catch {
}
try {
const filePath = typeof __filename !== "undefined" && __filename ? __filename : process.argv && process.argv[1] ? process.argv[1] : process.cwd();
const moduleDir = path.dirname(filePath);
return resolveProjectRootFrom(moduleDir);
} catch {
return resolveProjectRootFrom(process.cwd());
}
}
function getLogsDir() {
return path.join(getProjectRoot(), "logs");
}
// src/capabilities.ts
var cachedSelfPluginIdentifier;
var FALLBACK_SELF_PLUGIN_IDENTIFIER = "ceveyne/draw-things-chat";
function getSelfPluginIdentifier() {
if (cachedSelfPluginIdentifier !== void 0)
return cachedSelfPluginIdentifier;
try {
const root = getProjectRoot();
const manifestPath = path.join(root, "manifest.json");
const raw = fs.readFileSync(manifestPath, "utf-8");
const parsed = JSON.parse(raw);
const owner = typeof parsed?.owner === "string" ? parsed.owner.trim() : "";
const name = typeof parsed?.name === "string" ? parsed.name.trim() : "";
if (owner && name) {
cachedSelfPluginIdentifier = `${owner}/${name}`;
return cachedSelfPluginIdentifier;
}
} catch (err) {
console.warn(
"[Capabilities] Failed to resolve pluginIdentifier from manifest.json; using fallback.",
err
);
cachedSelfPluginIdentifier = FALLBACK_SELF_PLUGIN_IDENTIFIER;
return cachedSelfPluginIdentifier;
}
console.warn(
"[Capabilities] manifest.json did not contain valid owner/name; using fallback."
);
cachedSelfPluginIdentifier = FALLBACK_SELF_PLUGIN_IDENTIFIER;
return cachedSelfPluginIdentifier;
}
sdk.createConfigSchematics().field(
"model",
"string",
{
displayName: "Agent Model",
subtitle: "Enter the vision model to use as orchestrator. Default: Qwen3.6 35B \u0410\u0417\u0412.",
placeholder: "qwen/qwen3.6-35b-a3b"
},
"qwen/qwen3.6-35b-a3b"
).field(
"visionPromotionPersistent",
"boolean",
{
displayName: "Vision Promotion: Persistent",
subtitle: "ON: promote up to 5 attachments + 4 variants every turn. OFF: promote only when new.",
engineDoesNotSupport: true
},
false
).field(
"logRequests",
"boolean",
{
displayName: "Debug: Log requests/response",
subtitle: "Logs full request/response JSON; may include sensitive data.",
engineDoesNotSupport: true
},
false
).field(
"debugPromotion",
"boolean",
{
displayName: "Debug: Media promotion",
subtitle: "Verbose logs for media state, previews and cleanup.",
engineDoesNotSupport: true
},
false
).field(
"debugChunks",
"boolean",
{
displayName: "Debug: Stream chunk logs",
subtitle: "Log raw streaming chunks to console (verbose).",
engineDoesNotSupport: true
},
false
).build();
sdk.createConfigSchematics().field(
"baseUrl",
"string",
{
displayName: "LM Studio API base-URL",
subtitle: "Local LM Studio server base-URL. Default: http://127.0.0.1:1234/v1",
placeholder: "http://127.0.0.1:1234/v1"
},
"http://127.0.0.1:1234/v1"
).field(
"apiKey",
"string",
{
displayName: "(Optional) API Key",
subtitle: "Only needed if your LM Studio server requires authentication.",
isProtected: true,
placeholder: "sk-..."
},
""
).field(
"PREVIEW_IN_CHAT",
"boolean",
{
displayName: "Simple Previews in Chat",
subtitle: "When enabled, tool responses include client-based image previews.",
engineDoesNotSupport: false
},
false
).field(
"unloadAgentModelDuringRender",
"boolean",
{
displayName: "Unload Agent Model During Render",
subtitle: "When enabled, unloads the agent model from VRAM before long renders (image2image, edit, text2video, image2video). Only applies to local LM Studio instances.",
engineDoesNotSupport: false
},
true
).field(
"DRAW_THINGS_HOST",
"string",
{
displayName: "Draw Things Host",
subtitle: "Hostname or IP of the Draw Things backend server.",
placeholder: "127.0.0.1"
},
"127.0.0.1"
).field(
"DRAW_THINGS_HTTP_PORT",
"numeric",
{
displayName: "Draw Things HTTP Port",
subtitle: "HTTP API port (default: 7860)."
},
7860
).field(
"DRAW_THINGS_GRPC_PORT",
"numeric",
{
displayName: "Draw Things gRPC Port",
subtitle: "gRPC port (default: 7859)."
},
7859
).field(
"embedPngMetadata",
"boolean",
{
displayName: "Embed Metadata in PNGs",
subtitle: "Write generation parameters (prompt, model, seed, LoRAs, sources) into saved PNGs as XMP metadata. Compatible with draw-things-index.",
engineDoesNotSupport: false
},
true
).field(
"customConfigsPath",
"string",
{
displayName: "Custom Configs Path",
subtitle: "Path to custom_configs.json from Draw Things. Change to: `none` to disable.",
placeholder: "~/Library/Containers/com.liuliu.draw-things/Data/Documents/Models/custom_configs.json",
engineDoesNotSupport: false
},
"~/Library/Containers/com.liuliu.draw-things/Data/Documents/Models/custom_configs.json"
).field(
"HTTP_SERVER_PORT",
"numeric",
{
displayName: "Local HTTP Server Port",
subtitle: "Port for serving generated images over localhost (default: 54760).",
engineDoesNotSupport: true
},
54760
).build();
function getActiveChatContext(opts) {
return null;
}
var lmstudioHome = null;
function findLMStudioHome() {
if (lmstudioHome !== null) {
return lmstudioHome;
}
const resolvedHomeDir = fs.realpathSync(os.homedir());
const pointerFilePath = path.join(resolvedHomeDir, ".lmstudio-home-pointer");
if (fs.existsSync(pointerFilePath)) {
const candidate = fs.readFileSync(pointerFilePath, "utf-8").trim();
try {
if (candidate && fs.existsSync(candidate)) {
const hasConversations = fs.existsSync(path.join(candidate, "conversations"));
const hasUserFiles = fs.existsSync(path.join(candidate, "user-files"));
if (hasConversations || hasUserFiles) {
lmstudioHome = candidate;
return lmstudioHome;
}
}
} catch {
}
}
const dotHome = path.join(resolvedHomeDir, ".lmstudio");
const cacheHome = path.join(resolvedHomeDir, ".cache", "lm-studio");
const looksValid = (p) => {
try {
if (!fs.existsSync(p)) return false;
const conv = path.join(p, "conversations");
const files = path.join(p, "user-files");
return fs.existsSync(conv) || fs.existsSync(files);
} catch {
return false;
}
};
if (looksValid(dotHome)) {
lmstudioHome = dotHome;
try {
fs.writeFileSync(pointerFilePath, lmstudioHome, "utf-8");
} catch {
}
return lmstudioHome;
}
if (looksValid(cacheHome)) {
lmstudioHome = cacheHome;
try {
fs.writeFileSync(pointerFilePath, lmstudioHome, "utf-8");
} catch {
}
return lmstudioHome;
}
const home = dotHome;
lmstudioHome = home;
try {
fs.writeFileSync(pointerFilePath, lmstudioHome, "utf-8");
} catch {
}
return lmstudioHome;
}
function getLMStudioWorkingDir(chatId) {
const home = findLMStudioHome();
return path.join(home, "working-directories", chatId);
}
async function getLMStudioFileMetadata(fileIdentifier) {
try {
const home = findLMStudioHome() || path.join(os.homedir(), ".lmstudio");
const metadataPath = path.join(
home,
"user-files",
`${fileIdentifier}.metadata.json`
);
if (!fs.existsSync(metadataPath)) return null;
const raw = await fs.promises.readFile(metadataPath, "utf-8");
const meta = JSON.parse(raw);
if (typeof meta.originalName !== "string" || typeof meta.fileIdentifier !== "string") {
return null;
}
return meta;
} catch {
return null;
}
}
async function getOriginalFileName(fileIdentifier) {
const meta = await getLMStudioFileMetadata(fileIdentifier);
return meta?.originalName ?? null;
}
function localTimestamp(d = /* @__PURE__ */ new Date()) {
const pad = (n, len = 2) => String(n).padStart(len, "0");
const year = d.getFullYear();
const month = pad(d.getMonth() + 1);
const day = pad(d.getDate());
const hours = pad(d.getHours());
const minutes = pad(d.getMinutes());
const seconds = pad(d.getSeconds());
const millis = pad(d.getMilliseconds(), 3);
const tzOffset = -d.getTimezoneOffset();
const tzSign = tzOffset >= 0 ? "+" : "-";
const tzHours = pad(Math.floor(Math.abs(tzOffset) / 60));
const tzMins = pad(Math.abs(tzOffset) % 60);
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${millis}${tzSign}${tzHours}:${tzMins}`;
}
async function readState(chatWd) {
const p = path.join(chatWd, "chat_media_state.json");
try {
const raw = await fs.promises.readFile(p, "utf-8");
const json = JSON.parse(raw);
return normalizeState(json);
} catch {
return {
attachments: [],
variants: [],
pictures: [],
images: [],
counters: {}
};
}
}
async function writeStateAtomic(chatWd, state) {
const tmp = path.join(chatWd, "chat_media_state.json.tmp");
const dst = path.join(chatWd, "chat_media_state.json");
if (Array.isArray(state.attachments) && state.attachments.length > 1) {
state.attachments.sort((a, b) => (a.a ?? 0) - (b.a ?? 0));
}
if (Array.isArray(state.variants) && state.variants.length > 1) {
state.variants.sort((a, b) => (a.v ?? 0) - (b.v ?? 0));
}
if (Array.isArray(state.pictures) && state.pictures.length > 1) {
state.pictures.sort((a, b) => (a.p ?? 0) - (b.p ?? 0));
}
if (Array.isArray(state.images) && state.images.length > 1) {
state.images.sort((a, b) => (a.i ?? 0) - (b.i ?? 0));
}
let pretty = JSON.stringify(state, null, 2);
pretty = pretty.replace(
/"(lastPromotedAttachmentAs|lastPromotedVariantVs|lastPromotedImageIs|lastPixelPromotedAttachmentAs|lastPixelPromotedVariantVs|lastPixelPromotedImageIs|injectedMarkdown)":\s*\[\s*\n([\s\S]*?)\n\s*\]/g,
(match, key, content) => {
const items = content.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
return `"${key}": [${items.join(", ")}]`;
}
);
await fs.promises.writeFile(tmp, pretty, "utf-8");
await fs.promises.rename(tmp, dst);
}
function normalizeState(s) {
let lastPromotedAttachmentAs;
if (Array.isArray(s?.lastPromotedAttachmentAs)) {
lastPromotedAttachmentAs = s.lastPromotedAttachmentAs.filter(
(x) => typeof x === "number" && Number.isFinite(x)
);
} else if (Array.isArray(s?.lastPromotedAttachmentNs)) {
lastPromotedAttachmentAs = s.lastPromotedAttachmentNs.filter(
(x) => typeof x === "number" && Number.isFinite(x)
);
} else if (typeof s?.lastPromotedAttachmentN === "number") {
lastPromotedAttachmentAs = [s.lastPromotedAttachmentN];
} else if (typeof s?.lastPromotedAttachmentA === "number") {
lastPromotedAttachmentAs = [s.lastPromotedAttachmentA];
}
let lastPromotedVariantVs;
if (Array.isArray(s?.lastPromotedVariantVs)) {
lastPromotedVariantVs = s.lastPromotedVariantVs.filter(
(x) => typeof x === "number" && Number.isFinite(x)
);
}
let lastPromotedImageIs;
if (Array.isArray(s?.lastPromotedImageIs)) {
lastPromotedImageIs = s.lastPromotedImageIs.filter(
(x) => typeof x === "number" && Number.isFinite(x)
);
}
let lastPixelPromotedAttachmentAs;
if (Array.isArray(s?.lastPixelPromotedAttachmentAs)) {
lastPixelPromotedAttachmentAs = s.lastPixelPromotedAttachmentAs.filter(
(x) => typeof x === "number" && Number.isFinite(x)
);
} else if (Array.isArray(s?.lastPixelPromotedAttachmentNs)) {
lastPixelPromotedAttachmentAs = s.lastPixelPromotedAttachmentNs.filter(
(x) => typeof x === "number" && Number.isFinite(x)
);
}
let lastPixelPromotedVariantVs;
if (Array.isArray(s?.lastPixelPromotedVariantVs)) {
lastPixelPromotedVariantVs = s.lastPixelPromotedVariantVs.filter(
(x) => typeof x === "number" && Number.isFinite(x)
);
}
let lastPixelPromotedImageIs;
if (Array.isArray(s?.lastPixelPromotedImageIs)) {
lastPixelPromotedImageIs = s.lastPixelPromotedImageIs.filter(
(x) => typeof x === "number" && Number.isFinite(x)
);
}
const normalizeNumberArray = (v) => {
if (!Array.isArray(v)) return void 0;
const a = v.filter((x) => typeof x === "number" && Number.isFinite(x)).map((x) => Math.floor(x)).filter((x) => x > 0);
if (!a.length) return void 0;
return Array.from(new Set(a)).sort((x, y) => x - y);
};
const pendingReviewPromotion = (() => {
const pr = s?.pendingReviewPromotion;
if (!pr || typeof pr !== "object") return void 0;
const requestedAt = typeof pr.requestedAt === "string" && pr.requestedAt.trim() ? String(pr.requestedAt) : "";
const targets = pr.targets && typeof pr.targets === "object" ? pr.targets : null;
if (!requestedAt || !targets) return void 0;
const a = normalizeNumberArray(targets.a);
const v = normalizeNumberArray(targets.v);
const i = normalizeNumberArray(targets.i);
const p = normalizeNumberArray(targets.p);
if (!a && !v && !i && !p) return void 0;
const ttlMs = typeof pr.ttlMs === "number" && Number.isFinite(pr.ttlMs) && pr.ttlMs > 0 ? pr.ttlMs : void 0;
return {
requestedAt,
requestedByToolCallId: typeof pr.requestedByToolCallId === "string" && pr.requestedByToolCallId.trim() ? String(pr.requestedByToolCallId) : void 0,
reason: typeof pr.reason === "string" && pr.reason.trim() ? String(pr.reason) : void 0,
ttlMs,
targets: {
a,
v,
i,
p
}
};
})();
const pendingSequenceReview = (() => {
const ps = s?.pendingSequenceReview;
if (!ps || typeof ps !== "object") return void 0;
const requestedAt = typeof ps.requestedAt === "string" && ps.requestedAt.trim() ? String(ps.requestedAt) : "";
const movAbs = typeof ps.movAbs === "string" && ps.movAbs.trim() ? String(ps.movAbs) : "";
const variant = typeof ps.variant === "number" && Number.isFinite(ps.variant) ? Math.floor(ps.variant) : 0;
if (!requestedAt || !movAbs || variant <= 0) return void 0;
const fps = typeof ps.fps === "number" && Number.isFinite(ps.fps) && ps.fps > 0 ? ps.fps : 2;
const ttlMs = typeof ps.ttlMs === "number" && Number.isFinite(ps.ttlMs) && ps.ttlMs > 0 ? ps.ttlMs : void 0;
return {
requestedAt,
movAbs,
variant,
variantLabel: typeof ps.variantLabel === "string" && ps.variantLabel.trim() ? String(ps.variantLabel) : void 0,
fps,
ttlMs
};
})();
const n = {
attachments: Array.isArray(s?.attachments) ? s.attachments : [],
variants: Array.isArray(s?.variants) ? s.variants : [],
pictures: Array.isArray(s?.pictures) ? s.pictures : [],
images: Array.isArray(s?.images) ? s.images : [],
pendingReviewPromotion,
pendingSequenceReview,
lastEvent: s?.lastEvent,
lastCanvasNotation: typeof s?.lastCanvasNotation === "string" ? s.lastCanvasNotation : void 0,
lastCanvasAt: typeof s?.lastCanvasAt === "string" ? s.lastCanvasAt : void 0,
counters: typeof s?.counters === "object" && s?.counters ? s.counters : {},
injectedMarkdown: Array.isArray(s?.injectedMarkdown) ? s.injectedMarkdown : void 0,
injectedContent: Array.isArray(s?.injectedContent) ? s.injectedContent.filter((x) => typeof x === "string" && x.trim()) : void 0,
lastVariantsTs: typeof s?.lastVariantsTs === "string" ? s.lastVariantsTs : void 0,
lastPromotedTs: typeof s?.lastPromotedTs === "string" ? s.lastPromotedTs : void 0,
lastPromotedAttachmentAs,
// Keep deprecated field for backward compat during transition
lastPromotedAttachmentA: typeof s?.lastPromotedAttachmentA === "number" ? s.lastPromotedAttachmentA : typeof s?.lastPromotedAttachmentN === "number" ? s.lastPromotedAttachmentN : void 0,
lastPromotedVariantVs,
lastPromotedImageIs,
lastPixelPromotedAt: typeof s?.lastPixelPromotedAt === "string" ? s.lastPixelPromotedAt : void 0,
lastPixelPromotedAttachmentAs,
lastPixelPromotedVariantVs,
lastPixelPromotedImageIs,
forcePixelPromotionNextTurn: typeof s?.forcePixelPromotionNextTurn === "boolean" ? s.forcePixelPromotionNextTurn : void 0,
forcePixelPromotionSetAt: typeof s?.forcePixelPromotionSetAt === "string" ? s.forcePixelPromotionSetAt : void 0,
forcePixelPromotionReason: typeof s?.forcePixelPromotionReason === "string" ? s.forcePixelPromotionReason : void 0,
lastSsotMessageCount: typeof s?.lastSsotMessageCount === "number" && Number.isFinite(s.lastSsotMessageCount) ? s.lastSsotMessageCount : void 0
};
if (n.attachments.length > 1) {
n.attachments.sort((a, b) => (a.a ?? 0) - (b.a ?? 0));
}
if (n.variants.length > 1) {
n.variants.sort((a, b) => (a.v ?? 0) - (b.v ?? 0));
}
if (n.pictures.length > 1) {
n.pictures.sort((a, b) => (a.p ?? 0) - (b.p ?? 0));
}
if (n.images.length > 1) {
n.images.sort((a, b) => (a.i ?? 0) - (b.i ?? 0));
}
return n;
}
function normalizeString(val) {
return typeof val === "string" ? String(val) : void 0;
}
function normalizeNumber(val) {
return typeof val === "number" && Number.isFinite(val) ? val : void 0;
}
var COMMON_MEDIA_KEYS = /* @__PURE__ */ new Set([
"filename",
"preview",
"sourceTool",
"pluginId",
"sourceUrl",
"title",
"confidence",
"width",
"height",
"pageUrl",
"turnId",
"createdAt",
"kind",
"v",
"p",
"i"
]);
function buildMediaRecord(input, index, indexField, createdAt, existingIndex) {
const base = {
filename: input.filename,
preview: input.preview,
sourceTool: normalizeString(input.sourceTool),
pluginId: normalizeString(input.pluginId),
sourceUrl: normalizeString(input.sourceUrl),
title: normalizeString(input.title),
confidence: normalizeString(input.confidence),
width: normalizeNumber(input.width),
height: normalizeNumber(input.height),
pageUrl: normalizeString(input.pageUrl),
turnId: normalizeNumber(input.turnId),
createdAt: input.createdAt ?? createdAt
};
if (input.kind === "tool_result" || input.kind === "generated") {
base.kind = input.kind;
}
for (const key of Object.keys(input)) {
if (!COMMON_MEDIA_KEYS.has(key) && input[key] != null) {
base[key] = input[key];
}
}
base[indexField] = existingIndex ?? index;
return base;
}
function upgradeExistingRecord(existing, incoming, indexField) {
const incomingIndex = incoming[indexField];
if (existing[indexField] == null && typeof incomingIndex === "number") {
existing[indexField] = incomingIndex;
}
const fieldsToUpgrade = [
"sourceTool",
"pluginId",
"sourceUrl",
"title",
"confidence",
"pageUrl",
"kind"
];
for (const field of fieldsToUpgrade) {
if (existing[field] == null && incoming[field] != null) {
existing[field] = incoming[field];
}
}
if (existing.width == null && typeof incoming.width === "number") {
existing.width = incoming.width;
}
if (existing.height == null && typeof incoming.height === "number") {
existing.height = incoming.height;
}
if (existing.turnId == null && typeof incoming.turnId === "number") {
existing.turnId = incoming.turnId;
}
for (const key of Object.keys(incoming)) {
if (!COMMON_MEDIA_KEYS.has(key) && existing[key] == null && incoming[key] != null) {
existing[key] = incoming[key];
}
}
}
function appendMediaItems(state, items, config) {
if (!items.length) return { changed: false, state, records: [] };
const {
stateArrayKey,
counterKey,
indexField,
eventType,
getDedupeKey,
filter
} = config;
const existing = Array.isArray(
state[stateArrayKey]
) ? [...state[stateArrayKey]] : [];
const maxExistingIndex = existing.reduce((max, r) => {
const idx = r[indexField];
return Math.max(max, typeof idx === "number" && Number.isFinite(idx) ? idx : 0);
}, 0);
const counterIndex = state.counters[counterKey] ?? 1;
const baseIndex = Math.max(1, counterIndex, maxExistingIndex + 1);
const createdAt = localTimestamp();
let newOnes = items.map(
(it, idx) => buildMediaRecord(
it,
baseIndex + idx,
indexField,
createdAt,
it[indexField]
)
);
if (filter) {
newOnes = newOnes.filter(filter);
}
const existingByKey = /* @__PURE__ */ new Map();
for (const item of existing) {
const key = getDedupeKey(item);
if (key) existingByKey.set(key, item);
}
const all = [...existing];
for (const r of newOnes) {
const key = getDedupeKey(r);
const existingItem = key ? existingByKey.get(key) : void 0;
if (!existingItem) {
all.push(r);
if (key) existingByKey.set(key, r);
} else {
upgradeExistingRecord(
existingItem,
r,
indexField
);
}
}
all.sort((a, b) => {
const aIdx = a[indexField] ?? 0;
const bIdx = b[indexField] ?? 0;
return aIdx - bIdx || String(a.createdAt || "").localeCompare(String(b.createdAt || ""));
});
const changed = JSON.stringify(all) !== JSON.stringify(state[stateArrayKey] || []);
if (changed) {
state[stateArrayKey] = all;
const lastIndex = all.at(-1)?.[indexField];
state.counters[counterKey] = (typeof lastIndex === "number" ? lastIndex : baseIndex - 1) + 1;
state.lastEvent = { type: eventType, at: localTimestamp() };
}
return { changed, state, records: newOnes };
}
var IMAGE_APPEND_CONFIG = {
stateArrayKey: "images",
counterKey: "nextImageI",
indexField: "i",
eventType: "images",
getDedupeKey: (item) => `${item.filename}|${item.preview}`
};
function appendImages(state, items) {
return appendMediaItems(
state,
items,
IMAGE_APPEND_CONFIG
);
}
// src/services/drawthingsLimits.ts
var drawthingsLimits = {
// Limits for RENDERED output (requested_effective)
min: 256,
// Minimum dimension per side
maxWidth: 1536,
// Maximum render width
maxHeight: 1536,
// Preview generation for ALL MediaTypes (attachment, variant, image, picture).
// w + h ≤ previewMaxSum; one preview file serves: display in chat, vision promotion,
// analyse_image, detect_object.
previewMaxSum: 1792,
previewQuality: 80,
previewFormat: "jpeg"
};
// src/services/toolParams/variants.ts
function extractVariantUrisFromContent(raw) {
const candidates = [];
const text = typeof raw === "string" ? raw : JSON.stringify(raw);
const uriRegex = /file:\/\/[^\s"')]+generated-image-[^\s"')]*-v(\d+)\.png/gi;
let match;
while ((match = uriRegex.exec(text)) !== null) {
const uri = match[0];
const variantNum = parseInt(match[1], 10);
const filename = uri.split("/").pop() ?? "";
candidates.push({
filename,
index: variantNum
});
}
return candidates;
}
function extractGenerateImageResult(raw) {
const candidates = [];
if (raw && typeof raw === "object") {
const obj = raw;
if (Array.isArray(obj.filenames)) {
for (let i = 0; i < obj.filenames.length; i++) {
const fn = obj.filenames[i];
if (typeof fn === "string") {
candidates.push({ filename: fn, index: i + 1 });
}
}
}
if (Array.isArray(obj.content)) {
for (const item of obj.content) {
if (item && typeof item === "object" && "text" in item) {
const nested = extractVariantUrisFromContent(item.text);
candidates.push(...nested);
}
}
}
}
if (candidates.length === 0) {
return extractVariantUrisFromContent(raw);
}
return candidates;
}
var selfPlugin = getSelfPluginIdentifier() ?? "ceveyne/draw-things-chat";
var VARIANT_FULL_CONFIG = {
mediaType: "variant",
allow: "all",
ssotJoin: {
source: "conversation.json",
messageRole: "assistant",
jsonPath: "content",
// Regex scan for file:// URIs
extractFromSource: extractVariantUrisFromContent
},
scan: {
trigger: "both",
scope: "all-turns"
},
harvesting: {
defaultExtractor: extractGenerateImageResult,
tools: {
[`${selfPlugin}/generate_image`]: {
extractCandidates: extractGenerateImageResult,
actions: {
generatePreview: true,
visionPromotion: {
metadata: true,
pixel: true
}
}
}
}
},
actions: {
generatePreview: true,
visionPromotion: {
metadata: true,
// Labels (v1, v2) ALWAYS included
pixel: true
// Base64 pixels in rolling window
}
},
injectMdInAgentResponse: {
format: "none",
// Toggle-dependent: !PREVIEW_IN_CHAT
itemTemplate: "",
labelGenerator: (item, i) => `v${item.index ?? i + 1}`
},
preview: {
generate: true,
namingPattern: "preview-{basename}.jpg",
format: drawthingsLimits.previewFormat,
mimeType: "image/jpeg",
maxSum: drawthingsLimits.previewMaxSum,
quality: drawthingsLimits.previewQuality,
outputDir: "."
},
toggles: {
toggles: []
}
};
// src/helpers/imageUtils.ts
var loadedSharp2 = void 0;
var loadedJimp2 = void 0;
var libLogged = false;
var loggedFns = /* @__PURE__ */ new Set();
function logOnce(msg) {
if (loggedFns.has(msg)) return;
loggedFns.add(msg);
try {
console.debug(msg);
} catch {
}
}
async function tryLoadSharp2() {
if (loadedSharp2 !== void 0) return loadedSharp2;
try {
const mod = await import('sharp');
loadedSharp2 = mod?.default || mod;
if (!libLogged) {
try {
console.debug(`[imageUtils] using lib=sharp`);
libLogged = true;
} catch {
}
}
return loadedSharp2;
} catch {
loadedSharp2 = null;
return null;
}
}
async function tryLoadJimp2() {
if (loadedJimp2 !== void 0) return loadedJimp2;
try {
const mod = await import('jimp');
const candidate = mod && (mod.default || mod.Jimp || mod);
loadedJimp2 = candidate;
if (!libLogged) {
try {
console.debug(`[imageUtils] using lib=jimp`);
libLogged = true;
} catch {
}
}
return loadedJimp2;
} catch {
loadedJimp2 = null;
return null;
}
}
function jimpHasFn(obj, name) {
try {
return obj && typeof obj[name] === "function";
} catch {
return false;
}
}
async function jimpResizeCompat(img, w, h) {
if (!jimpHasFn(img, "resize")) {
throw new Error("Jimp resize not available");
}
let lastError;
try {
await img.resize({ w, h });
return;
} catch (e) {
lastError = e;
}
try {
await img.resize(w, h);
return;
} catch (e) {
lastError = e;
}
throw new Error(
`Jimp resize failed for both API variants: ${lastError?.message || String(lastError)}`
);
}
async function jimpAutoRotate(img) {
try {
if (jimpHasFn(img, "exifRotate")) {
await img.exifRotate();
} else if (jimpHasFn(img, "rotate")) {
}
} catch (e) {
try {
console.error(`[imageUtils] Jimp EXIF rotation failed: ${String(e)}`);
} catch {
}
}
}
async function jimpGetBufferCompat(img, mime, options) {
let lastError;
try {
if (jimpHasFn(img, "getBufferAsync")) {
return await img.getBufferAsync(mime, options);
}
} catch (e) {
lastError = e;
}
try {
if (jimpHasFn(img, "getBuffer")) {
return await img.getBuffer(mime, options);
}
} catch (e) {
lastError = e;
}
throw new Error(
`Jimp getBuffer failed for both API variants (mime=${mime}): ${lastError?.message || String(lastError)}`
);
}
async function getSize(buffer) {
const sharp = await tryLoadSharp2();
if (sharp) {
logOnce(`[imageUtils.getSize] using Sharp`);
const meta = await sharp(buffer).rotate().metadata();
return { width: meta.width || 0, height: meta.height || 0 };
}
const Jimp = await tryLoadJimp2();
if (Jimp && typeof Jimp.read === "function") {
logOnce(`[imageUtils.getSize] using Jimp`);
const img = await Jimp.read(buffer);
await jimpAutoRotate(img);
const w = typeof img.getWidth === "function" ? img.getWidth() : typeof img.width === "number" ? img.width : img.bitmap?.width || 0;
const h = typeof img.getHeight === "function" ? img.getHeight() : typeof img.height === "number" ? img.height : img.bitmap?.height || 0;
return { width: w || 0, height: h || 0 };
}
return { width: 0, height: 0 };
}
async function resizeAndEncode(buffer, format, quality, width, _maxBytes) {
const sharp = await tryLoadSharp2();
if (sharp) {
logOnce(`[imageUtils.resizeAndEncode] using Sharp`);
const pipeline = sharp(buffer).rotate().resize({ width, fit: "inside", withoutEnlargement: false });
const { data, info } = await (format === "jpeg" ? pipeline.jpeg({
quality: clampQuality(quality),
mozjpeg: true,
chromaSubsampling: "4:2:0",
progressive: true
}) : pipeline.webp({ quality: clampQuality(quality), effort: 4 })).toBuffer({ resolveWithObject: true });
const outW = typeof info.width === "number" ? info.width : width;
const outH = typeof info.height === "number" ? info.height : Math.round(width * 0.75);
return { data, width: outW, height: outH };
}
const Jimp = await tryLoadJimp2();
if (Jimp && typeof Jimp.read === "function") {
logOnce(`[imageUtils.resizeAndEncode] using Jimp`);
const img = await Jimp.read(buffer);
await jimpAutoRotate(img);
const origW = typeof img.getWidth === "function" ? img.getWidth() : typeof img.width === "number" ? img.width : img.bitmap?.width || 0;
const origH = typeof img.getHeight === "function" ? img.getHeight() : typeof img.height === "number" ? img.height : img.bitmap?.height || 0;
const scale = width / Math.max(1, origW);
const outW = Math.max(1, Math.round(origW * scale));
const outH = Math.max(1, Math.round(origH * scale));
await jimpResizeCompat(img, outW, outH);
const q = clampQuality(quality);
{
const data = await jimpGetBufferCompat(img, "image/jpeg", { quality: q });
return { data, width: outW, height: outH };
}
}
return { data: buffer, width, height: Math.round(width * 0.75) };
}
async function resizeAndEncodeByHeight(buffer, format, quality, maxHeight) {
const sharp = await tryLoadSharp2();
if (sharp) {
logOnce(`[imageUtils.resizeAndEncodeByHeight] using Sharp`);
const pipeline = sharp(buffer).rotate().resize({ height: maxHeight, fit: "inside", withoutEnlargement: false });
const { data, info } = await (format === "jpeg" ? pipeline.jpeg({
quality: clampQuality(quality),
mozjpeg: true,
chromaSubsampling: "4:2:0",
progressive: true
}) : pipeline.webp({ quality: clampQuality(quality), effort: 4 })).toBuffer({ resolveWithObject: true });
const outW = typeof info.width === "number" ? info.width : Math.round(maxHeight * 1.33);
const outH = typeof info.height === "number" ? info.height : maxHeight;
return { data, width: outW, height: outH };
}
const Jimp = await tryLoadJimp2();
if (Jimp && typeof Jimp.read === "function") {
logOnce(`[imageUtils.resizeAndEncodeByHeight] using Jimp`);
const img = await Jimp.read(buffer);
await jimpAutoRotate(img);
const origW = typeof img.getWidth === "function" ? img.getWidth() : typeof img.width === "number" ? img.width : img.bitmap?.width || 0;
const origH = typeof img.getHeight === "function" ? img.getHeight() : typeof img.height === "number" ? img.height : img.bitmap?.height || 0;
const scale = maxHeight / Math.max(1, origH);
const outH = Math.max(1, Math.round(maxHeight));
const outW = Math.max(1, Math.round(origW * scale));
await jimpResizeCompat(img, outW, outH);
const q = clampQuality(quality);
{
const data = await jimpGetBufferCompat(img, "image/jpeg", { quality: q });
return { data, width: outW, height: outH };
}
}
return {
data: buffer,
width: Math.round(maxHeight * 1.33),
height: maxHeight
};
}
async function resizeAndEncodeBySum(buffer, format, quality, maxSum) {
const sharp = await tryLoadSharp2();
const calcDims = (origW, origH) => {
let w = Math.max(1, Math.round(origW));
let h = Math.max(1, Math.round(origH));
const currentSum = w + h;
if (currentSum > maxSum) {
const scale = maxSum / currentSum;
w = Math.max(1, Math.round(w * scale));
h = Math.max(1, Math.round(h * scale));
}
return { w, h };
};
if (sharp) {
logOnce(`[imageUtils.resizeAndEncodeBySum] using Sharp`);
const metadata = await sharp(buffer).metadata();
const origW = metadata.width ?? 640;
const origH = metadata.height ?? 640;
const { w, h } = calcDims(origW, origH);
const pipeline = sharp(buffer).rotate().resize({ width: w, height: h, fit: "inside", withoutEnlargement: true });
const { data, info } = await (format === "jpeg" ? pipeline.jpeg({
quality: clampQuality(quality),
mozjpeg: true,
chromaSubsampling: "4:2:0",
progressive: true
}) : pipeline.webp({ quality: clampQuality(quality), effort: 4 })).toBuffer({ resolveWithObject: true });
const outW = typeof info.width === "number" ? info.width : w;
const outH = typeof info.height === "number" ? info.height : h;
return { data, width: outW, height: outH };
}
const Jimp = await tryLoadJimp2();
if (Jimp && typeof Jimp.read === "function") {
logOnce(`[imageUtils.resizeAndEncodeBySum] using Jimp`);
const img = await Jimp.read(buffer);
await jimpAutoRotate(img);
const origW = typeof img.getWidth === "function" ? img.getWidth() : typeof img.width === "number" ? img.width : img.bitmap?.width || 640;
const origH = typeof img.getHeight === "function" ? img.getHeight() : typeof img.height === "number" ? img.height : img.bitmap?.height || 640;
const { w, h } = calcDims(origW, origH);
await jimpResizeCompat(img, w, h);
const q = clampQuality(quality);
{
const data = await jimpGetBufferCompat(img, "image/jpeg", { quality: q });
return { data, width: w, height: h };
}
}
return {
data: buffer,
width: Math.round(maxSum / 2),
height: Math.round(maxSum / 2)
};
}
function clampQuality(q) {
if (!Number.isFinite(q)) return 80;
q = Math.round(q);
if (q < 1) q = 1;
if (q > 100) q = 100;
return q;
}
// src/media-promotion-core/image.ts
function isAllowedOriginalExt(p) {
return /(\.(png|jpe?g|webp|mov))$/i.test(p);
}
function previewFilenameFrom(originalFilename) {
const hasPrefix = originalFilename.toLowerCase().startsWith("preview-");
const base = hasPrefix ? originalFilename : `preview-${originalFilename}`;
return base.replace(/\.(png|jpg|jpeg|webp|gif)$/i, ".jpg");
}
async function encodeJpegPreviewFromBuffer(srcBuf, opts) {
const q = Math.max(1, Math.min(100, opts.quality));
if (opts.mode === "height") {
const targetH = Math.max(1, Math.round(opts.maxDim));
const { data: data2, width: width2, height: height2 } = await resizeAndEncodeByHeight(
srcBuf,
"jpeg",
q,
targetH
);
return { data: data2, width: width2, height: height2 };
}
if (opts.mode === "sum" && opts.maxSum) {
const { data: data2, width: width2, height: height2 } = await resizeAndEncodeBySum(
srcBuf,
"jpeg",
q,
opts.maxSum
);
return { data: data2, width: width2, height: height2 };
}
const maxW = Math.max(1, Math.round(opts.maxDim));
const { data, width, height } = await resizeAndEncode(
srcBuf,
"jpeg",
q,
maxW);
return { data, width, height };
}
async function encodeJpegPreview(srcAbs, dstAbs, opts) {
const srcBuf = await fs.promises.readFile(srcAbs);
const { data } = await encodeJpegPreviewFromBuffer(srcBuf, opts);
await fs.promises.writeFile(dstAbs, data);
}
function isPreviewOptions(x) {
return typeof x === "object" && x !== null && "maxDim" in x && typeof x.maxDim === "number";
}
function normalizeToPreviewOptions(input) {
if (isPreviewOptions(input)) return input;
return {
maxDim: input.maxWidth ?? 640,
quality: input.quality ?? 80,
mode: input.maxSum ? "sum" : "width",
maxSum: input.maxSum
};
}
async function generatePreview(srcAbs, chatWd, optsInput, options) {
const debug = options?.debug ?? false;
const opts = normalizeToPreviewOptions(optsInput);
if (!fs.existsSync(srcAbs)) {
if (debug) console.warn(`[Preview] Source not found: ${srcAbs}`);
return null;
}
const originalFilename = path.basename(srcAbs);
const previewFilename = options?.customFilename ?? previewFilenameFrom(originalFilename);
const previewAbs = path.join(chatWd, previewFilename);
if (!options?.force && fs.existsSync(previewAbs)) {
if (debug) console.info(`[Preview] Exists, skipping: ${previewFilename}`);
return previewFilename;
}
try {
await encodeJpegPreview(srcAbs, previewAbs, opts);
if (debug) console.info(`[Preview] Generated: ${previewFilename}`);
return previewFilename;
} catch (e) {
if (debug)
console.warn(
`[Preview] Failed for ${originalFilename}:`,
e.message
);
throw e;
}
}
async function generatePreviewFromBuffer(srcBuf, chatWd, originalFilename, optsInput, options) {
const debug = false;
const opts = normalizeToPreviewOptions(optsInput);
const previewFilename = options?.customFilename ?? previewFilenameFrom(originalFilename);
const previewAbs = path.join(chatWd, previewFilename);
if (fs.existsSync(previewAbs)) {
try {
const existingData = await fs.promises.readFile(previewAbs);
return {
previewFilename,
previewAbs,
data: existingData,
width: 0,
// Unknown for existing file
height: 0
};
} catch {
}
}
try {
const { data, width, height } = await encodeJpegPreviewFromBuffer(
srcBuf,
opts
);
await fs.promises.writeFile(previewAbs, data);
if (debug)
;
return {
previewFilename,
previewAbs,
data,
width,
height
};
} catch (e) {
throw e;
}
}
function findConversationPath(chatWd) {
const chatId = path.basename(chatWd);
const lmHome = findLMStudioHome() || path.join(os.homedir(), ".lmstudio");
const conversationsDir = path.join(lmHome, "conversations");
const candidates = [
path.join(conversationsDir, `${chatId}.conversation.json`),
path.join(chatWd, ".conversation.json"),
path.join(chatWd, "conversation.json")
];
for (const p of candidates) {
try {
fs.accessSync(p, fs.constants.F_OK);
return p;
} catch {
}
}
return void 0;
}
async function readConversation(chatWd) {
const p = findConversationPath(chatWd);
if (!p) return void 0;
const maxAttempts = 5;
const retryDelayMs = 50;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const raw = await fs.promises.readFile(p, "utf-8");
const json = JSON.parse(raw);
return { json, path: p };
} catch {
if (attempt < maxAttempts - 1) {
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
}
}
}
return void 0;
}
function findMessagesArray(json) {
if (!json || typeof json !== "object") return void 0;
const obj = json;
const candidates = [
obj.messages,
obj.conversation?.messages,
obj.chat?.messages,
obj.history,
obj.turns,
obj.items
];
for (const arr of candidates) {
if (Array.isArray(arr) && arr.length > 0) {
return arr;
}
}
return void 0;
}
function resolveMessageVersion(raw) {
if (!raw || typeof raw !== "object") return raw;
const obj = raw;
const versions = obj.versions;
if (!Array.isArray(versions) || versions.length === 0) {
return raw;
}
const selRaw = obj.currentlySelected;
const sel = typeof selRaw === "number" && Number.isFinite(selRaw) ? selRaw : 0;
if (sel >= 0 && sel < versions.length) {
return versions[sel];
}
return versions[versions.length - 1];
}
function getMessageRole(msg) {
if (!msg || typeof msg !== "object") return "unknown";
const obj = msg;
if (obj.type === "contentBlock") {
const arr = obj.content;
if (Array.isArray(arr)) {
for (const it of arr) {
if (!it || typeof it !== "object") continue;
const t = it.type;
if (t === "toolCallRequest" || t === "toolCallResult") {
return "tool";
}
}
}
}
const role = obj.role ?? obj.author ?? obj.sender;
if (typeof role === "string") return role.toLowerCase();
const type = obj.type ?? obj.messageType;
if (typeof type === "string") {
if (type === "user" || type === "user_message") return "user";
if (type === "assistant" || type === "assistant_message")
return "assistant";
if (type === "system") return "system";
if (type === "tool" || type === "tool_result") return "tool";
}
return "unknown";
}
function parseMessages(json) {
const messages = findMessagesArray(json);
if (!messages) return [];
const result = [];
for (let i = 0; i < messages.length; i++) {
const raw = messages[i];
const resolved = resolveMessageVersion(raw);
const role = getMessageRole(resolved);
result.push({
index: i,
turnId: i + 1,
// 1-based
role,
content: resolved,
raw
});
}
return result;
}
function extractUserAttachments(msg, lmHome) {
const result = [];
const home = findLMStudioHome() || path.join(os.homedir(), ".lmstudio");
const userFilesDir = path.join(home, "user-files");
const content = msg.content;
if (!content || typeof content !== "object") return result;
const collectFromObject = (obj) => {
if (!obj || typeof obj !== "object") return;
if (Array.isArray(obj)) {
for (const item of obj) {
collectFromObject(item);
}
return;
}
const o = obj;
const fileId = o.fileIdentifier ?? o.file_identifier ?? o.identifier;
const fileType = o.fileType ?? o.file_type ?? o.type;
if (typeof fileId === "string" && fileId.trim()) {
if (fileType === "image" || /\.(png|jpg|jpeg|webp|gif|bmp|tiff?)$/i.test(fileId)) {
result.push(path.join(userFilesDir, fileId));
}
}
const filePath = o.path ?? o.filePath ?? o.file_path ?? o.uri ?? o.url;
if (typeof filePath === "string" && filePath.trim()) {
let resolved = filePath;
if (filePath.startsWith("file://")) {
try {
resolved = decodeURIComponent(filePath.replace(/^file:\/\//, ""));
} catch {
resolved = filePath.replace(/^file:\/\//, "");
}
}
if (/\.(png|jpg|jpeg|webp|gif|bmp|tiff?)$/i.test(resolved)) {
result.push(resolved);
}
}
for (const key of Object.keys(o)) {
if (key !== "content" || !Array.isArray(o[key])) {
collectFromObject(o[key]);
}
}
};
const contentArray = content.content;
if (Array.isArray(contentArray)) {
for (const part of contentArray) {
collectFromObject(part);
}
} else {
collectFromObject(content);
}
const files = content.files;
if (Array.isArray(files)) {
for (const f of files) {
collectFromObject(f);
}
}
const attachments = content.attachments;
if (Array.isArray(attachments)) {
for (const a of attachments) {
collectFromObject(a);
}
}
return result;
}
function extractPendingAttachments(json, lmHome) {
const result = [];
const home = findLMStudioHome() || path.join(os.homedir(), ".lmstudio");
const userFilesDir = path.join(home, "user-files");
if (!json || typeof json !== "object") return result;
const files = json.clientInputFiles;
if (!Array.isArray(files)) return result;
for (const f of files) {
if (!f || typeof f !== "object") continue;
const fo = f;
const id = fo.fileIdentifier;
const type = fo.fileType;
if (typeof id === "string" && id.trim() && type === "image") {
result.push(path.join(userFilesDir, id));
}
}
return result;
}
function buildConversationWideRequestMetadata(messages, debug) {
const metaByKey = /* @__PURE__ */ new Map();
let toolRequestCount = 0;
const remember = (key, meta) => {
const k = typeof key === "string" || typeof key === "number" ? String(key) : "";
if (!k) return;
const prev = metaByKey.get(k) ?? {};
metaByKey.set(k, {
pluginId: meta.pluginId ?? prev.pluginId,
toolName: meta.toolName ?? prev.toolName
});
};
if (debug) {
console.info(
`[MediaScanner] buildConversationWideRequestMetadata: scanning ${messages.length} messages`
);
}
for (const msg of messages) {
const content = msg.content;
if (!content || typeof content !== "object") continue;
const obj = content;
if (obj.type === "contentBlock" && Array.isArray(obj.content)) {
for (const item of obj.content) {
if (!item || typeof item !== "object") continue;
const bo = item;
if (bo.type !== "toolCallRequest") continue;
const pluginId = typeof bo.pluginIdentifier === "string" ? bo.pluginIdentifier : void 0;
const toolName = typeof bo.name === "string" ? bo.name : void 0;
const callId = bo.callId ?? bo.toolCallId ?? bo.id;
const reqId = bo.toolCallRequestId ?? bo.requestId;
toolRequestCount++;
if (debug) {
console.info(
`[MediaScanner] Request metadata (Case1): callId=${callId} reqId=${reqId} tool=${toolName} plugin=${pluginId ?? "(none)"}`
);
}
remember(callId, { pluginId, toolName });
remember(reqId, { pluginId, toolName });
}
}
if (Array.isArray(obj.content)) {
for (const item of obj.content) {
if (!item || typeof item !== "object") continue;
const it = item;
if (it.type === "contentBlock" && Array.isArray(it.content)) {
for (const bi of it.content) {
if (!bi || typeof bi !== "object") continue;
const bo = bi;
if (bo.type !== "toolCallRequest") continue;
const pluginId = typeof bo.pluginIdentifier === "string" ? bo.pluginIdentifier : void 0;
const toolName = typeof bo.name === "string" ? bo.name : void 0;
const callId = bo.callId ?? bo.toolCallId ?? bo.id;
const reqId = bo.toolCallRequestId ?? bo.requestId;
toolRequestCount++;
if (debug) {
console.info(
`[MediaScanner] Request metadata (Case2): callId=${callId} reqId=${reqId} tool=${toolName} plugin=${pluginId ?? "(none)"}`
);
}
remember(callId, { pluginId, toolName });
remember(reqId, { pluginId, toolName });
}
}
}
}
if (Array.isArray(obj.steps)) {
for (const step of obj.steps) {
if (!step || typeof step !== "object") continue;
const st = step;
if (st.type === "contentBlock" && Array.isArray(st.content)) {
for (const bi of st.content) {
if (!bi || typeof bi !== "object") continue;
const bo = bi;
if (bo.type !== "toolCallRequest") continue;
const pluginId = typeof bo.pluginIdentifier === "string" ? bo.pluginIdentifier : void 0;
const toolName = typeof bo.name === "string" ? bo.name : void 0;
const callId = bo.callId ?? bo.toolCallId ?? bo.id;
const reqId = bo.toolCallRequestId ?? bo.requestId;
toolRequestCount++;
if (debug) {
console.info(
`[MediaScanner] Request metadata (Case3-steps): callId=${callId} reqId=${reqId} tool=${toolName} plugin=${pluginId ?? "(none)"}`
);
}
remember(callId, { pluginId, toolName });
remember(reqId, { pluginId, toolName });
}
}
}
}
}
if (debug) {
console.info(
`[MediaScanner] buildConversationWideRequestMetadata: found ${toolRequestCount} toolCallRequests, ${metaByKey.size} unique keys`
);
}
return metaByKey;
}
async function scanMedia(chatWd, mediaType, options) {
const { scope, debug = false } = options;
if (!chatWd) {
return { candidates: [] };
}
const conv = await readConversation(chatWd);
if (!conv) {
if (debug) {
console.info(`[MediaScanner] No conversation.json found for ${chatWd}`);
}
return { candidates: [] };
}
const messages = parseMessages(conv.json);
if (debug) {
console.info(
`[MediaScanner] Parsed ${messages.length} messages from ${conv.path}`
);
}
const requestMetaByKey = buildConversationWideRequestMetadata(
messages,
debug
);
if (debug && requestMetaByKey.size > 0) {
console.info(
`[MediaScanner] Collected ${requestMetaByKey.size} request metadata entries`
);
}
const candidates = [];
const orderedMessages = scope === "last" ? [...messages].reverse() : messages;
for (const msg of orderedMessages) {
let foundInThisMessage = [];
{
foundInThisMessage = scanAttachmentsInMessage(msg, conv.json);
}
if (foundInThisMessage.length > 0) {
candidates.push(...foundInThisMessage);
if (scope === "last") {
break;
}
}
}
if (scope === "all") {
const pending = extractPendingAttachments(conv.json);
const toAppend = [];
for (const p of pending) {
const exists = candidates.some((c) => c.identifier === p);
if (exists) continue;
toAppend.push({
kind: "attachment",
identifier: p,
turnId: 0
// Pending = before any turn
});
}
if (toAppend.length) {
candidates.push(...toAppend);
}
}
if (debug) {
console.info(
`[MediaScanner] FINAL: Found ${candidates.length} ${mediaType} candidates (scope: ${scope})`
);
if (candidates.length > 0) {
for (const c of candidates.slice(0, 3)) {
console.info(
`[MediaScanner] candidate: id=${c.identifier?.slice(
0,
50
)} pluginId=${c.pluginId} tool=${c.sourceTool}`
);
}
}
}
return {
candidates,
conversationPath: conv.path
};
}
function scanAttachmentsInMessage(msg, conversationJson, debug) {
if (msg.role !== "user") return [];
const attachments = extractUserAttachments(msg);
return attachments.map((absPath) => ({
kind: "attachment",
identifier: absPath,
turnId: msg.turnId
}));
}
async function findAllMedia(chatWd, mediaType, debug = false) {
return scanMedia(chatWd, mediaType, { scope: "all", debug });
}
// src/services/mediaScanner/legacyAdapters.ts
async function findAllAttachmentsLegacy(chatWd, debug) {
if (!chatWd) return { found: [], turnIdByAbs: {} };
const result = await findAllMedia(chatWd, "attachment", debug);
const found = [];
const turnIdByAbs = {};
for (const c of result.candidates) {
const abs = c.identifier;
if (!found.includes(abs)) {
found.push(abs);
}
if (turnIdByAbs[abs] === void 0) {
turnIdByAbs[abs] = c.turnId;
}
}
return { found, turnIdByAbs };
}
async function pathExists2(p) {
try {
await fs.promises.access(p, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
async function importAttachmentBatch(chatWd, state, sourcePaths, turnIdByOriginAbs, previewOpts, maxPreviewAttachments = 0, debug = false) {
const normalizeAbs = (p) => {
try {
return path.resolve(p);
} catch {
return p;
}
};
const normalizedSource = sourcePaths.filter((p) => typeof p === "string" && p.trim().length > 0).map(normalizeAbs);
const normalizeTurnIdMap = (m) => {
if (!m) return {};
const out = {};
for (const [k, v] of Object.entries(m)) {
if (typeof v === "number" && Number.isFinite(v)) {
out[normalizeAbs(k)] = v;
}
}
return out;
};
const normalizedTurnIdByAbs = normalizeTurnIdMap(turnIdByOriginAbs);
const normalizedSourceDeduped = [];
{
const seen = /* @__PURE__ */ new Set();
for (const p of normalizedSource) {
if (!seen.has(p)) {
seen.add(p);
normalizedSourceDeduped.push(p);
}
}
}
if (normalizedSourceDeduped.length === 0) {
if (debug)
console.info(
"Batch import: No new attachments in current turn; keeping existing state."
);
return { changed: false };
}
try {
const current = Array.isArray(state.attachments) ? state.attachments : [];
const currentOrigins = current.map(
(a) => a && typeof a.originAbs === "string" ? String(a.originAbs) : ""
).filter((p) => p.trim().length > 0).map(normalizeAbs);
const same = currentOrigins.length === normalizedSourceDeduped.length && currentOrigins.every((p, i) => p === normalizedSourceDeduped[i]);
if (same && current.length > 0) {
let allOk = true;
for (let i = 0; i < current.length; i++) {
const a = current[i];
const pv = a && typeof a.preview === "string" ? String(a.preview) : "";
if (i < Math.max(0, Math.floor(maxPreviewAttachments))) {
if (!pv) {
allOk = false;
break;
}
const pvAbs = path.join(chatWd, pv);
const okPv = await pathExists2(pvAbs);
if (!okPv) {
allOk = false;
break;
}
}
}
if (allOk) {
try {
let anyMetaChanged = false;
const nextAttachments = current.map((a) => {
const originAbs = a && typeof a.originAbs === "string" ? String(a.originAbs) : "";
const key = originAbs ? normalizeAbs(originAbs) : "";
const nextTurnId = key ? normalizedTurnIdByAbs[key] : void 0;
let updated = a;
if (typeof nextTurnId === "number" && a && a.turnId !== nextTurnId) {
anyMetaChanged = true;
updated = { ...updated, turnId: nextTurnId };
}
if (!updated.preview && originAbs) {
const candidate = previewFilenameFrom(path.basename(originAbs));
if (fs.existsSync(path.join(chatWd, candidate))) {
anyMetaChanged = true;
updated = { ...updated, preview: candidate };
}
}
return updated;
});
if (anyMetaChanged) {
state.attachments = nextAttachments;
await writeStateAtomic(chatWd, state);
if (debug)
console.info(
"Batch import: updated attachment turnId metadata (idempotent, no re-import)."
);
return { changed: false, metadataChanged: true };
}
} catch (e) {
if (debug)
console.warn(
"Batch import: turnId metadata update failed; continuing idempotent skip:",
e.message
);
}
if (debug)
console.info(
"Batch import: SSOT matches current state; skipping re-import (idempotent)."
);
return { changed: false };
}
}
} catch (e) {
if (debug)
console.warn(
"Batch import: idempotence check failed; continuing with import:",
e.message
);
}
const existingByOrigin = /* @__PURE__ */ new Map();
for (const a of state.attachments || []) {
if (a && typeof a.originAbs === "string") {
existingByOrigin.set(normalizeAbs(a.originAbs), a);
}
}
const usedAs = /* @__PURE__ */ new Set();
for (const a of existingByOrigin.values()) {
const av = a?.a;
if (typeof av === "number" && Number.isFinite(av) && av > 0) {
usedAs.add(av);
}
}
const ensureNextA = () => {
const current = state?.counters?.nextAttachmentA;
if (typeof current === "number" && Number.isFinite(current) && current > 0) {
return Math.floor(current);
}
const maxExisting = usedAs.size ? Math.max(...Array.from(usedAs)) : 0;
return maxExisting + 1;
};
let nextA = ensureNextA();
const allocateA = () => {
while (usedAs.has(nextA)) nextA++;
const a = nextA;
usedAs.add(a);
nextA++;
return a;
};
const imported = [];
for (let i = 0; i < normalizedSourceDeduped.length; i++) {
const abs = normalizedSourceDeduped[i];
if (!await pathExists2(abs)) {
if (debug)
console.warn(`Batch import: source not found, skipping: ${abs}`);
continue;
}
if (!isAllowedOriginalExt(abs)) {
if (debug)
console.warn(`Batch import: extension not allowed, skipping: ${abs}`);
continue;
}
const existing = existingByOrigin.get(normalizeAbs(abs));
if (existing) {
if (existing.preview) {
const previewAbs = path.join(chatWd, existing.preview);
if (!await pathExists2(previewAbs)) {
if (debug)
console.info(
`Batch import: regenerating missing preview for a${existing.a}`
);
await generatePreview(abs, chatWd, previewOpts, {
customFilename: existing.preview,
force: true,
debug
});
}
}
const nextTurnId = normalizedTurnIdByAbs[normalizeAbs(abs)];
let existingWidth = existing.width;
let existingHeight = existing.height;
if (existingWidth == null || existingHeight == null) {
try {
const origBuf = await fs.promises.readFile(abs);
const dims = await getSize(origBuf);
if (dims.width > 0 && dims.height > 0) {
existingWidth = dims.width;
existingHeight = dims.height;
}
} catch (e) {
if (debug)
console.warn(
`Batch import: failed to measure dims for a${existing.a}:`,
e.message
);
}
}
imported.push({
...existing,
// Keep stable `a`
a: typeof existing.a === "number" && Number.isFinite(existing.a) && existing.a > 0 ? existing.a : allocateA(),
turnId: typeof nextTurnId === "number" ? nextTurnId : existing.turnId,
width: existingWidth,
height: existingHeight
});
if (debug)
console.info(
`Batch import: reusing existing attachment as a${existing.a}: ${path.basename(abs)}`
);
continue;
}
const origin = path.basename(abs);
let previewName = void 0;
if (i < Math.max(0, Math.floor(maxPreviewAttachments))) {
previewName = await generatePreview(abs, chatWd, previewOpts, { debug }) ?? void 0;
}
if (!previewName) {
const candidate = previewFilenameFrom(path.basename(abs));
if (fs.existsSync(path.join(chatWd, candidate))) {
previewName = candidate;
}
}
let originalName = void 0;
const lmHome = findLMStudioHome();
const userFilesDir = path.join(lmHome, "user-files");
if (abs.startsWith(userFilesDir)) {
const fileIdentifier = path.basename(abs);
const resolvedName = await getOriginalFileName(fileIdentifier);
if (!resolvedName || !resolvedName.trim()) {
throw new Error(
`Missing originalName in LM Studio metadata for fileIdentifier='${fileIdentifier}' (abs='${abs}')`
);
}
originalName = resolvedName;
if (debug)
console.info(
`Resolved original filename: ${fileIdentifier} \u2192 ${originalName}`
);
} else {
originalName = path.basename(abs);
}
let origWidth;
let origHeight;
try {
const origBuf = await fs.promises.readFile(abs);
const dims = await getSize(origBuf);
if (dims.width > 0 && dims.height > 0) {
origWidth = dims.width;
origHeight = dims.height;
}
} catch (e) {
if (debug)
console.warn(
`Batch import: failed to measure dims for new attachment ${path.basename(abs)}:`,
e.message
);
}
imported.push({
origin,
originAbs: abs,
originalName,
turnId: normalizedTurnIdByAbs[normalizeAbs(abs)],
preview: previewName,
width: origWidth,
height: origHeight,
createdAt: localTimestamp(),
a: allocateA()
// Stable, monotonically increasing id
});
if (debug)
console.info(
`Batch import: new attachment as a${imported[imported.length - 1].a}: ${path.basename(abs)}`
);
}
if (imported.length === 0) {
if (debug) console.warn("Batch import: no valid attachments imported");
return { changed: false };
}
state.attachments = imported;
state.counters.nextAttachmentA = nextA;
state.lastEvent = { type: "attachment", at: localTimestamp() };
await writeStateAtomic(chatWd, state);
if (debug)
console.info(
`Batch imported ${imported.length} attachment(s) from SSOT (replaced array)`
);
return { changed: true };
}
// src/helpers/attachmentSync.ts
var DEFAULT_PREVIEW_OPTS = { maxDim: drawthingsLimits.previewMaxSum, quality: drawthingsLimits.previewQuality, mode: "sum", maxSum: drawthingsLimits.previewMaxSum };
async function syncAttachmentsToState(workingDir, debug = false, maxPreviewAttachments = 0, previewOpts) {
const state = await readState(workingDir);
const { found, turnIdByAbs } = await findAllAttachmentsLegacy(
workingDir,
debug
);
const opts = maxPreviewAttachments > 0 ? DEFAULT_PREVIEW_OPTS : { maxDim: 0, quality: 0 };
const result = await importAttachmentBatch(
workingDir,
state,
found,
turnIdByAbs,
opts,
maxPreviewAttachments,
debug
);
return { changed: result.changed };
}
function buildPaths() {
const logsDir = getLogsDir();
const filePath = path.resolve(logsDir, "generate-image-plugin.audit.jsonl");
return { logsDir, filePath };
}
function localTimestamp2() {
try {
return (/* @__PURE__ */ new Date()).toLocaleString(void 0, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
timeZoneName: "short"
});
} catch {
return (/* @__PURE__ */ new Date()).toString();
}
}
function buildAuditLogger({
backend,
mode,
requestId: providedRequestId
}) {
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const entry = {
timestamp: localTimestamp2(),
requestId,
backend,
mode
};
function setChatId(id) {
if (typeof id === "string" && id.trim().length > 0) entry.chat_id = id;
}
function setUserRequest(req) {
entry.user_request = req;
}
function setRenderTarget(target) {
entry.render_target = target;
}
function setInputs(inputs) {
entry.inputs = inputs;
}
function setOutput(output) {
entry.output = { ...entry.output, ...output };
}
function setError(err) {
let message = "unknown error";
let status = void 0;
if (typeof err === "string") message = err;
else if (err && typeof err === "object") {
const anyErr = err;
message = anyErr.message || JSON.stringify(anyErr);
if (typeof anyErr.status === "number") status = anyErr.status;
}
entry.error = status ? { message, status } : { message };
}
async function write() {
try {
const { logsDir, filePath } = buildPaths();
await fs.promises.mkdir(logsDir, { recursive: true });
const block = JSON.stringify(entry, null, 2) + "\n\n";
await fs.promises.appendFile(filePath, block, { encoding: "utf8" });
} catch (e) {
console.error(
"auditLog write failed:",
e instanceof Error ? e.message : String(e)
);
}
}
return {
requestId,
setChatId,
setUserRequest,
setRenderTarget,
setInputs,
setOutput,
setError,
write
};
}
var DEFAULT_HOST = "127.0.0.1";
function envPort() {
const v = process.env.HTTP_SERVER_PORT;
if (v == null || String(v).trim() === "") return void 0;
const n = Number(v);
return Number.isInteger(n) && n >= 1024 && n <= 65535 ? n : void 0;
}
async function healthCheck(port, host = DEFAULT_HOST) {
return new Promise((resolve) => {
const req = http.get(
{ host, port, path: "/__healthz", timeout: 600 },
(res) => {
try {
const ok = (res.statusCode || 0) === 200 && String(res.headers["x-mcp-image-server"]) === "1";
res.resume();
res.once("end", () => resolve(ok));
} catch {
resolve(false);
}
}
);
req.on("timeout", () => {
try {
req.destroy();
} catch {
}
resolve(false);
});
req.on("error", () => resolve(false));
});
}
function toHttpOriginalUrl(fileName, baseUrl, chatId) {
if (chatId) {
return `${baseUrl.replace(/\/$/, "")}/${encodeURIComponent(
chatId
)}/${encodeURIComponent(fileName)}`;
}
return `${baseUrl.replace(/\/$/, "")}/${encodeURIComponent(fileName)}`;
}
function toHttpPreviewUrl(fileName, baseUrl, chatId) {
if (chatId) {
return `${baseUrl.replace(/\/$/, "")}/${encodeURIComponent(
chatId
)}/${encodeURIComponent(fileName)}`;
}
return `${baseUrl.replace(/\/$/, "")}/previews/${encodeURIComponent(
fileName
)}`;
}
async function getHealthyServerBaseUrl(host = DEFAULT_HOST) {
try {
const fixedPort = envPort();
if (fixedPort == null) return "";
const ok = await healthCheck(fixedPort, host).catch(() => false);
if (ok) return `http://127.0.0.1:${fixedPort}`;
return "";
} catch {
return "";
}
}
function scoreToConfidence(score) {
if (score >= 2) return "high";
if (score === 1) return "medium";
return "low";
}
async function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function resolveActiveLMStudioChatId(opts) {
const retries = 4;
const delayMs = 200;
const recentSec = 120;
try {
const home = findLMStudioHome();
const convDir = path.join(home, "conversations");
if (!fs.existsSync(convDir)) {
return {
ok: false,
reason: `LM Studio conversations dir not found: ${convDir}`
};
}
let chosenPath = null;
let mtimeMs = 0;
for (let attempt = 0; attempt < Math.max(1, retries); attempt++) {
const entries = await fs.promises.readdir(convDir).catch(() => []);
const convFiles = entries.filter((f) => f.endsWith(".conversation.json")).map((f) => path.join(convDir, f));
if (convFiles.length === 0) {
if (attempt < retries - 1) {
await sleep(delayMs);
continue;
}
return { ok: false, reason: "No conversation files found" };
}
const withTimes = convFiles.map((p) => {
try {
const s = fs.statSync(p);
return s.isFile() ? { p, t: s.mtimeMs } : null;
} catch {
return null;
}
}).filter(Boolean);
if (withTimes.length === 0) {
if (attempt < retries - 1) {
await sleep(delayMs);
continue;
}
return { ok: false, reason: "No readable conversation files" };
}
withTimes.sort((a, b) => b.t - a.t);
chosenPath = withTimes[0].p;
mtimeMs = withTimes[0].t;
try {
const raw = await fs.promises.readFile(chosenPath, "utf8");
JSON.parse(raw);
break;
} catch {
if (attempt < retries - 1) {
await sleep(delayMs);
continue;
}
break;
}
}
if (!chosenPath)
return { ok: false, reason: "Failed to pick conversation" };
const chatId = path.basename(chosenPath).replace(/\.conversation\.json$/i, "");
let score = 0;
let reason = [];
if (mtimeMs > 0) {
const ageSec = (Date.now() - mtimeMs) / 1e3;
if (ageSec <= recentSec) {
score += 1;
reason.push(`recent:${Math.round(ageSec)}s`);
} else {
reason.push(`stale:${Math.round(ageSec)}s`);
}
}
try {
const raw = await fs.promises.readFile(chosenPath, "utf8");
JSON.parse(raw);
score += 1;
reason.push("parse_ok");
} catch {
reason.push("parse_uncertain");
}
return {
ok: true,
chatId,
filePath: chosenPath,
mtimeMs,
confidence: scoreToConfidence(score),
reason: reason.join(",")
};
} catch (e) {
return { ok: false, reason: e?.message || String(e) };
}
}
// src/helpers/resolveImg2ImgSourceLMStudio.ts
var LM_HOME = path.join(os.homedir(), ".lmstudio");
try {
const h = findLMStudioHome();
if (h && typeof h === "string") LM_HOME = h;
} catch {
}
util.promisify(child_process.exec);
// src/interfaces/control.ts
__toESM(require_flatbuffers());
// src/interfaces/generation-configuration.ts
__toESM(require_flatbuffers());
// src/interfaces/lo-ra.ts
__toESM(require_flatbuffers());
// src/interfaces/tensor-history-node.ts
__toESM(require_flatbuffers());
// src/interfaces/thumbnail-history-node.ts
__toESM(require_flatbuffers());
// src/interfaces/text-history-node.ts
__toESM(require_flatbuffers());
var TOOL_MIN_RENDER_DIM = drawthingsLimits.min;
var TOOL_MAX_WIDTH = drawthingsLimits.maxWidth;
var TOOL_MAX_HEIGHT = drawthingsLimits.maxHeight;
var TOOL_MAX_PREVIEW_W = drawthingsLimits.maxWidth;
var ZOOM_TOOL_MAX_DIM = 2048;
function normalizeQualityToInt(q, def) {
let n = typeof q === "string" ? parseFloat(q) : typeof q === "number" ? q : def;
if (!Number.isFinite(n)) n = def;
if (n > 0 && n <= 1) n = n * 100;
n = Math.round(n);
if (n < 1) n = 1;
if (n > 100) n = 100;
return n;
}
var GenerateToolParamsSchemaBase = zod.z.object({
prompt: zod.z.string().optional(),
negative_prompt: zod.z.string().optional(),
width: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM, `width must be >= ${TOOL_MIN_RENDER_DIM}`).max(TOOL_MAX_WIDTH, `width must be <= ${TOOL_MAX_WIDTH}`).optional(),
height: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM, `height must be >= ${TOOL_MIN_RENDER_DIM}`).max(TOOL_MAX_HEIGHT, `height must be <= ${TOOL_MAX_HEIGHT}`).optional(),
steps: zod.z.coerce.number().int().min(1, "steps must be >= 1").max(50, "steps must be <= 50").optional(),
seed: zod.z.coerce.number().int().optional(),
guidance_scale: zod.z.coerce.number().min(0, "guidance_scale must be >= 0").max(50, "guidance_scale must be <= 50").optional(),
model: zod.z.string().optional(),
sampler: zod.z.union([zod.z.string(), zod.z.number()]).optional(),
numFrames: zod.z.coerce.number().int().min(1, "numFrames must be >= 1").max(641, "numFrames must be <= 641").optional(),
random_string: zod.z.string().optional(),
// preview controls (validation only; defaults applied by caller)
previewFormat: zod.z.enum(["jpeg", "webp"], {
errorMap: () => ({ message: "previewFormat must be 'jpeg' or 'webp'" })
}).optional(),
previewMaxWidth: zod.z.coerce.number().int().min(128).max(TOOL_MAX_PREVIEW_W).optional(),
previewMinWidth: zod.z.coerce.number().int().min(128).max(TOOL_MAX_PREVIEW_W).optional(),
previewMaxBytes: zod.z.coerce.number().int().min(2e3).max(2e5).optional(),
previewQuality: zod.z.union([zod.z.coerce.number(), zod.z.string()]).transform((v) => normalizeQualityToInt(v, NaN)).optional(),
previewMinQuality: zod.z.union([zod.z.coerce.number(), zod.z.string()]).transform((v) => normalizeQualityToInt(v, NaN)).optional(),
previewQualityStep: zod.z.coerce.number().int().min(1).max(20).optional(),
previewScaleStep: zod.z.coerce.number().min(0.5).max(0.98).optional(),
previewInChat: zod.z.coerce.boolean().optional(),
alt: zod.z.string().max(120).optional(),
// saving
saveOriginal: zod.z.coerce.boolean().optional(),
saveDir: zod.z.string().optional()
}).passthrough();
GenerateToolParamsSchemaBase.superRefine((d, ctx) => {
if (d.previewMinWidth !== void 0 && d.previewMaxWidth !== void 0 && d.previewMinWidth > d.previewMaxWidth) {
ctx.addIssue({
code: "custom",
path: ["previewMinWidth"],
message: "previewMinWidth must be <= previewMaxWidth"
});
}
if (d.previewMinQuality !== void 0 && d.previewQuality !== void 0 && d.previewMinQuality > d.previewQuality) {
ctx.addIssue({
code: "custom",
path: ["previewMinQuality"],
message: "previewMinQuality must be <= previewQuality"
});
}
});
zod.z.union([
zod.z.string().transform(
(s) => s.split(/[\s,]+/).map((x) => parseInt(x.trim(), 10)).filter((n) => Number.isInteger(n) && n >= 1)
),
zod.z.coerce.number().int().min(1).transform((n) => [n]),
zod.z.array(zod.z.coerce.number().int().min(1))
]).optional();
function normalizeSourceToken(raw) {
let s = String(raw ?? "").trim();
if (!s) return "";
s = s.replace(/^[\[{(]+/, "").replace(/[\]})]+$/, "");
s = s.replace(/^['"`]+|['"`]+$/g, "");
s = s.replace(/[;:.!?]+$/, "");
s = s.trim();
if (/^a$/i.test(s)) return "a1";
if (/^v$/i.test(s)) return "v1";
if (/^p$/i.test(s)) return "p1";
if (/^i$/i.test(s)) return "i1";
return s;
}
var SourceNotation = zod.z.preprocess(
(v) => typeof v === "string" ? normalizeSourceToken(v) : v,
zod.z.string().trim().regex(
/^([avpi]|[avpi]?[1-9]\d*)$/i,
"Source notation: 'a1', 'v2', 'p1', 'i3', or digit when unambiguous"
)
);
var SourceNotationList = zod.z.union([
// String form: split by comma/space and validate each part
zod.z.string().transform(
(s) => s.split(/[\s,]+/).map((x) => normalizeSourceToken(x)).filter((x) => x.length > 0)
),
// Array form: validate each element
zod.z.array(SourceNotation)
]).refine(
(arr) => arr.every((x) => /^([avpi]|[avpi]?[1-9]\d*)$/i.test(String(x))),
"moodboard contains invalid source notation(s)"
);
var GenerateToolParamsShapeMinimal = {
prompt: zod.z.string().optional().describe(
"Image description (mode: 'text2image') OR description of desired changes (mode: 'image2image')."
),
width: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM).max(TOOL_MAX_WIDTH).optional().describe(`Width in pixels (max ${TOOL_MAX_WIDTH}). Has sensible default.`),
height: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM).max(TOOL_MAX_HEIGHT).optional().describe(`Height in pixels (max ${TOOL_MAX_HEIGHT}). Has sensible default.`),
imageFormat: zod.z.enum(["square", "landscape", "portrait", "16:9"]).optional().describe("Aspect ratio shorthand. Override if context suggests. '16:9' yields 1024\xD7576 (video-optimized)."),
quality: zod.z.enum(["low", "medium", "high", "auto"]).optional().describe("Quality preset (affects steps). Default is balanced."),
// model preset selection — default 'auto' uses built-in settings
model: zod.z.string().optional().describe("Model preset. 'ltx' selects LTX-2 for video generation. Default 'auto' selects best model for the mode."),
// number of images to generate in one call
variants: zod.z.coerce.number().int().min(1).max(4).optional().describe("Number of images (1-4). Default is 1."),
// number of video frames
numFrames: zod.z.coerce.number().int().min(1, "numFrames must be >= 1").max(641, "numFrames must be <= 641").optional().describe("Number of video frames. Must be a multiple of 32 (or multiple of 32 + 1). Default: 1 (image). Silently ignored for non-video modes."),
// image2image controls (only allow selecting a prior variant)
mode: zod.z.enum(["text2image", "image2image", "edit", "text2video", "image2video", "refine"]).optional().describe("Generation mode. 'text2video'/'image2video' require a video-capable model (e.g. 'ltx'). Required when sources exist."),
// Primary source for image2image/edit
canvas: zod.z.union([
SourceNotation,
zod.z.array(SourceNotation).min(1).max(1).transform((arr) => arr[0])
]).optional().describe(
"Primary source image. Notation: 'a1', 'v2', 'p1', 'i3'. Digit-only allowed only when unambiguous. Tolerates single-item array input."
),
// Additional style references for image2image/edit modes (gRPC only for image2image)
moodboard: SourceNotationList.optional().describe(
"Additional style references for image2image/edit modes. Array of source notations (same format as canvas). Note: moodboard for image2image requires gRPC transport."
)
};
zod.z.object(GenerateToolParamsShapeMinimal).strict();
var cropSideField = zod.z.union([zod.z.number(), zod.z.string()]).optional();
var CropToolParamsShape = {
canvas: zod.z.union([
SourceNotation,
zod.z.array(SourceNotation).min(1).max(1).transform((arr) => arr[0])
]).optional().describe(
"Source image. Notation: 'a1', 'v2', 'p1', 'i3'. Tolerates single-item array input."
),
cropLeft: cropSideField.describe("Amount to remove from the left side. Default unit: % (0\u201399). Optionally append 'px' for absolute pixels, e.g. '120px'. Default 0."),
cropRight: cropSideField.describe("Amount to remove from the right side. Default unit: % (0\u201399). Optionally append 'px' for absolute pixels, e.g. '120px'. Default 0."),
cropTop: cropSideField.describe("Amount to remove from the top. Default unit: % (0\u201399). Optionally append 'px' for absolute pixels, e.g. '80px'. Default 0."),
cropBottom: cropSideField.describe("Amount to remove from the bottom. Default unit: % (0\u201399). Optionally append 'px' for absolute pixels, e.g. '80px'. Default 0."),
imageFormat: zod.z.enum(["square", "landscape", "portrait", "16:9"]).optional().describe(
"Target aspect ratio. Only active when explicitly set; omitting it crops with the given sides only (no AR enforcement). Ignored when all 4 crop sides are explicitly given. Unspecified axes are centred; single-side anchors are honoured. 'square'=1:1, 'landscape'=4:3, 'portrait'=3:4, '16:9'=16:9."
),
detectLabel: zod.z.string().optional().describe(
"Crop to the bounding box of a detected object by label (requires a prior detect_object run). canvas may be the original source (e.g. 'a1') or the detect_object result (e.g. 'i3')."
),
detectIndex: zod.z.coerce.number().int().min(0).optional().describe("Zero-based index to select among multiple detections with the same label. Default 0."),
frameAdjust: zod.z.union([zod.z.number(), zod.z.string()]).optional().describe(
"Expand (positive) or shrink (negative) the detection bounding box before cropping. Number: percent of min(W,H). String: value + optional 'px' suffix, e.g. '20px' or '-10%'."
)
};
zod.z.object(CropToolParamsShape).strict();
var ZoomInToolParamsShape = {
prompt: zod.z.string().optional().describe("Description of desired changes to apply during re-rendering. Default: empty (preserve content)."),
width: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM).max(ZOOM_TOOL_MAX_DIM).optional().describe(`Output width in pixels (max ${ZOOM_TOOL_MAX_DIM}). Has sensible default.`),
height: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM).max(ZOOM_TOOL_MAX_DIM).optional().describe(`Output height in pixels (max ${ZOOM_TOOL_MAX_DIM}). Has sensible default.`),
quality: zod.z.enum(["low", "medium", "high", "auto"]).optional().describe("Quality preset (affects steps). Default is balanced."),
model: zod.z.string().optional().describe("Model preset for image2image re-rendering. Default 'auto'."),
canvas: zod.z.union([
SourceNotation,
zod.z.array(SourceNotation).min(1).max(1).transform((arr) => arr[0])
]).optional().describe(
"Source image. Notation: 'a1', 'v2', 'p1', 'i3'. Tolerates single-item array input."
),
detectLabel: zod.z.string().optional().describe(
"Crop to the bounding box of a detected object by label (requires a prior detect_object run). canvas may be the original source (e.g. 'a1') or the detect_object result (e.g. 'i3')."
),
detectIndex: zod.z.coerce.number().int().min(0).optional().describe("Zero-based index to select among multiple detections with the same label. Default 0."),
frameAdjust: zod.z.union([zod.z.number(), zod.z.string()]).optional().describe(
"Expand (positive) or shrink (negative) the detection bounding box before cropping. Number: percent of min(W,H). String: value + optional 'px' suffix, e.g. '20px' or '-10%'."
),
imageFormat: zod.z.enum(["square", "landscape", "portrait", "16:9"]).optional().describe(
"Target aspect ratio for the render output. Only active when explicitly set. 'square'=1:1, 'landscape'=4:3, 'portrait'=3:4, '16:9'=16:9."
)
};
zod.z.object(ZoomInToolParamsShape).strict();
var InpaintToolParamsShape = {
prompt: zod.z.string().optional().describe("Description of desired changes to apply during re-rendering. Default: empty (preserve content)."),
width: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM).max(ZOOM_TOOL_MAX_DIM).optional().describe(`Output width in pixels (max ${ZOOM_TOOL_MAX_DIM}). Has sensible default.`),
height: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM).max(ZOOM_TOOL_MAX_DIM).optional().describe(`Output height in pixels (max ${ZOOM_TOOL_MAX_DIM}). Has sensible default.`),
quality: zod.z.enum(["low", "medium", "high", "auto"]).optional().describe("Quality preset (affects steps). Default is balanced."),
model: zod.z.string().optional().describe("Model preset for image2image re-rendering. Default 'auto'."),
canvas: zod.z.union([
SourceNotation,
zod.z.array(SourceNotation).min(1).max(1).transform((arr) => arr[0])
]).optional().describe(
"Source image. Notation: 'a1', 'v2', 'p1', 'i3'. Tolerates single-item array input."
),
detectLabel: zod.z.string().optional().describe(
"Crop to the bounding box of a detected object by label (requires a prior detect_object run). canvas may be the original source (e.g. 'a1') or the detect_object result (e.g. 'i3')."
),
detectIndex: zod.z.coerce.number().int().min(0).optional().describe("Zero-based index to select among multiple detections with the same label. Default 0."),
frameAdjust: zod.z.union([zod.z.number(), zod.z.string()]).optional().describe(
"Expand (positive) or shrink (negative) the detection bounding box before cropping. Number: percent of min(W,H). String: value + optional 'px' suffix, e.g. '20px' or '-10%'."
)
};
zod.z.object(InpaintToolParamsShape).strict();
var OutpaintToolParamsShape = {
prompt: zod.z.string().optional().describe("Description of desired changes to apply during re-rendering. Default: empty (preserve content)."),
width: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM).max(ZOOM_TOOL_MAX_DIM).optional().describe(`Output width in pixels (max ${ZOOM_TOOL_MAX_DIM}). Has sensible default.`),
height: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM).max(ZOOM_TOOL_MAX_DIM).optional().describe(`Output height in pixels (max ${ZOOM_TOOL_MAX_DIM}). Has sensible default.`),
quality: zod.z.enum(["low", "medium", "high", "auto"]).optional().describe("Quality preset (affects steps). Default is balanced."),
model: zod.z.string().optional().describe("Model preset for image2image re-rendering. Default 'auto'."),
canvas: zod.z.union([
SourceNotation,
zod.z.array(SourceNotation).min(1).max(1).transform((arr) => arr[0])
]).optional().describe(
"Source image. Notation: 'a1', 'v2', 'p1', 'i3'. Tolerates single-item array input."
),
detectLabel: zod.z.string().optional().describe(
"Crop to the bounding box of a detected object by label (requires a prior detect_object run). canvas may be the original source (e.g. 'a1') or the detect_object result (e.g. 'i3')."
),
detectIndex: zod.z.coerce.number().int().min(0).optional().describe("Zero-based index to select among multiple detections with the same label. Default 0."),
frameAdjust: zod.z.union([zod.z.number(), zod.z.string()]).optional().describe(
"Expand (positive) or shrink (negative) the detection bounding box before cropping. Number: percent of min(W,H). String: value + optional 'px' suffix, e.g. '20px' or '-10%'."
)
};
zod.z.object(OutpaintToolParamsShape).strict();
var UpscaleToolParamsShape = {
prompt: zod.z.string().optional().describe("Description of desired changes to apply during re-rendering. Default: empty (preserve content)."),
width: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM).max(ZOOM_TOOL_MAX_DIM).optional().describe(`Output width in pixels (max ${ZOOM_TOOL_MAX_DIM}). Has sensible default.`),
height: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM).max(ZOOM_TOOL_MAX_DIM).optional().describe(`Output height in pixels (max ${ZOOM_TOOL_MAX_DIM}). Has sensible default.`),
scaleFactor: zod.z.number().positive().optional().describe("Scale factor applied to the canvas dimensions. E.g. 2 doubles the resolution. Mutually exclusive with width/height."),
quality: zod.z.enum(["low", "medium", "high", "auto"]).optional().describe("Quality preset (affects steps). Default is balanced."),
model: zod.z.string().optional().describe("Model preset for image2image re-rendering. Default 'auto'."),
canvas: zod.z.union([
SourceNotation,
zod.z.array(SourceNotation).min(1).max(1).transform((arr) => arr[0])
]).optional().describe(
"Source image. Notation: 'a1', 'v2', 'p1', 'i3'. Tolerates single-item array input."
),
imageFormat: zod.z.string().optional().describe("Target image format / aspect ratio preset (e.g. '16:9', 'portrait'). Resolved to concrete pixel dimensions.")
};
zod.z.object(UpscaleToolParamsShape).strict();
var RefineToolParamsShape = {
canvas: zod.z.union([
SourceNotation,
zod.z.array(SourceNotation).min(1).max(1).transform((arr) => arr[0])
]).optional().describe(
"Source image. Notation: 'a1', 'v2', 'p1', 'i3'. Tolerates single-item array input."
),
model: zod.z.string().describe(
"Required. Model preset to use for refinement. model: z-image produces a polished, refined look. model: qwen-image or model: flux produces a more natural, organic look."
),
width: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM).max(ZOOM_TOOL_MAX_DIM).optional().describe(`Output width in pixels (max ${ZOOM_TOOL_MAX_DIM}). Defaults to canvas width.`),
height: zod.z.coerce.number().int().min(TOOL_MIN_RENDER_DIM).max(ZOOM_TOOL_MAX_DIM).optional().describe(`Output height in pixels (max ${ZOOM_TOOL_MAX_DIM}). Defaults to canvas height.`),
imageFormat: zod.z.string().optional().describe("Target image format / aspect ratio preset (e.g. '16:9', 'portrait'). Resolved to concrete pixel dimensions.")
};
zod.z.object(RefineToolParamsShape).strict();
zod.z.union([
// String form: split by comma/space
zod.z.string().transform(
(s) => s.split(/[\s,]+/).map((x) => x.trim()).filter((x) => x.length > 0)
),
// Array form: pass through
zod.z.array(zod.z.string())
]).refine((arr) => arr.length >= 1, "targets must contain at least one notation").refine((arr) => arr.length <= 32, "targets must contain at most 32 notations");
function readPngGenerationMeta(filePath){let buf;try{buf=fs.readFileSync(filePath);}catch{return null}if(buf.length<8||buf[0]!==137||buf[1]!==80||buf[2]!==78||buf[3]!==71){return null}let offset=8;while(offset+12<=buf.length){const chunkLen=buf.readUInt32BE(offset);const chunkType=buf.toString("ascii",offset+4,offset+8);const dataStart=offset+8;const dataEnd=dataStart+chunkLen;if(dataEnd+4>buf.length)break;if(chunkType==="IEND")break;if(chunkType==="iTXt"){const data=buf.slice(dataStart,dataEnd);const kwEnd=data.indexOf(0);if(kwEnd>=0&&data.toString("ascii",0,kwEnd)==="XML:com.adobe.xmp"){const comprFlag=data[kwEnd+1];if(comprFlag!==0){offset=dataEnd+4;continue}let pos=kwEnd+3;while(pos<data.length&&data[pos]!==0)pos++;pos++;while(pos<data.length&&data[pos]!==0)pos++;pos++;const xmpText=data.toString("utf8",pos);return extractMetaFromXmp(xmpText)}}offset=dataEnd+4;}return null}function extractMetaFromXmp(xmp){const match=xmp.match(/<exif:UserComment>[\s\S]*?<rdf:li[^>]*>([\s\S]*?)<\/rdf:li>/);if(!match)return null;let raw;try{const jsonText=match[1].trim().replace(/"/g,'"').replace(/'/g,"'").replace(/</g,"<").replace(/>/g,">").replace(/&/g,"&");raw=JSON.parse(jsonText);}catch{return null}const loras=Array.isArray(raw.lora)?raw.lora.filter(l=>l&&typeof l.model==="string").map(l=>({file:String(l.model),weight:typeof l.weight==="number"?l.weight:undefined})):undefined;const sources=Array.isArray(raw.sources)?raw.sources.filter(s=>typeof s==="string"):undefined;return {prompt:typeof raw.c==="string"&&raw.c?raw.c:undefined,negativePrompt:typeof raw.uc==="string"&&raw.uc?raw.uc:undefined,model:typeof raw.model==="string"&&raw.model?raw.model:undefined,sampler:typeof raw.sampler==="string"&&raw.sampler?raw.sampler:undefined,steps:typeof raw.steps==="number"?raw.steps:undefined,guidanceScale:typeof raw.scale==="number"?raw.scale:undefined,seed:typeof raw.seed==="number"?raw.seed:undefined,seedMode:typeof raw.seed_mode==="string"&&raw.seed_mode?raw.seed_mode:undefined,shift:typeof raw.shift==="number"?raw.shift:undefined,size:typeof raw.size==="string"&&raw.size?raw.size:undefined,strength:typeof raw.strength==="number"?raw.strength:undefined,loras:loras?.length?loras:undefined,sources:sources?.length?sources:undefined,mode:typeof raw.mode==="string"&&raw.mode?raw.mode:undefined,generatedBy:typeof raw.generated_by==="string"&&raw.generated_by?raw.generated_by:undefined}}function formatGenerationMeta(meta){const lines=[" GENERATION METADATA:"];if(meta.prompt)lines.push(` Prompt: ${meta.prompt}`);if(meta.negativePrompt)lines.push(` Negative Prompt: ${meta.negativePrompt}`);if(meta.model)lines.push(` Model: ${meta.model}`);const techParts=[];if(meta.sampler)techParts.push(`Sampler: ${meta.sampler}`);if(typeof meta.steps==="number")techParts.push(`Steps: ${meta.steps}`);if(typeof meta.guidanceScale==="number")techParts.push(`Guidance Scale: ${meta.guidanceScale}`);if(typeof meta.seed==="number")techParts.push(`Seed: ${meta.seed}`);if(techParts.length>0)lines.push(` ${techParts.join(" ")}`);if(meta.size)lines.push(` Size: ${meta.size}`);if(typeof meta.strength==="number"&&meta.strength!==1){lines.push(` Strength: ${meta.strength}`);}if(meta.loras&&meta.loras.length>0){const loraStr=meta.loras.map(l=>l.weight!=null?`${l.file} (${l.weight})`:l.file).join(", ");lines.push(` LoRA: ${loraStr}`);}if(meta.sources&&meta.sources.length>0){lines.push(` Source(s): ${meta.sources.join(", ")}`);}return lines.join("\n")}
async function analyzeMlxVisionBatch(items,config){if(!items.length){return {results:[],totalInferenceTimeMs:0,backend:"mlx"}}const images=[];for(const it of items){const buf=await fs.promises.readFile(it.filePath);const b64=buf.toString("base64");images.push({id:it.id,data:b64});}const payload={images};if(config.prompt!==undefined&&config.prompt!==""){payload.prompt=config.prompt;}if(config.maxTokens!==undefined){payload.max_tokens=config.maxTokens;}if(config.temperature!==undefined){payload.temperature=config.temperature;}const controller=new AbortController;const timeoutMs=config.timeoutMs;const timeout=setTimeout(()=>controller.abort(),timeoutMs);let data;try{const resp=await fetch(config.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload),signal:controller.signal});clearTimeout(timeout);if(!resp.ok){const detail=await resp.text().catch(()=>"(no body)");throw new Error(`MLX Vision API ${resp.status}: ${detail}`)}data=await resp.json();}catch(e){clearTimeout(timeout);throw new Error(`MLX Vision API /analyze failed: ${e.message||String(e)}`)}const results=Array.isArray(data?.results)?data.results.map(r=>({id:String(r?.id||""),text:String(r?.text||"").trim(),inferenceTimeMs:typeof r?.inference_time_ms==="number"?r.inference_time_ms:0})):[];const totalInferenceTimeMs=typeof data?.total_inference_time_ms==="number"?data.total_inference_time_ms:0;const backend=typeof data?.backend==="string"?data.backend:"mlx";return {results,totalInferenceTimeMs,backend}}function bboxToCrop(bbox,W,H){const[x1,y1,x2,y2]=bbox;return {cropLeft:x1/W*100,cropRight:(W-x2)/W*100,cropTop:y1/H*100,cropBottom:(H-y2)/H*100}}async function analyzeMlxDetectionBatch(items,config){if(!items.length){return {results:[],totalInferenceTimeMs:0,backend:"florence2"}}const images=[];for(const it of items){const buf=await fs.promises.readFile(it.filePath);const b64=buf.toString("base64");images.push({id:it.id,data:b64});}const payload={images,task:config.task??"<OD>",florence2_model_path:config.florence2ModelPath};const controller=new AbortController;const timeoutMs=config.timeoutMs;const timeout=setTimeout(()=>controller.abort(),timeoutMs);let data;try{const resp=await fetch(config.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload),signal:controller.signal});clearTimeout(timeout);if(!resp.ok){const detail=await resp.text().catch(()=>"(no body)");throw new Error(`Florence-2 /detect API ${resp.status}: ${detail}`)}data=await resp.json();}catch(e){clearTimeout(timeout);throw new Error(`Florence-2 /detect API failed: ${e.message||String(e)}`)}const results=Array.isArray(data?.results)?data.results.map(r=>{const W=typeof r?.width==="number"?r.width:0;const H=typeof r?.height==="number"?r.height:0;const rawBboxes=Array.isArray(r?.bboxes)?r.bboxes:[];const rawLabels=Array.isArray(r?.labels)?r.labels:[];const objects=rawBboxes.map((bbox,idx)=>({label:rawLabels[idx]??"",bbox,...bboxToCrop(bbox,W,H)}));return {id:String(r?.id||""),objects,imageWidth:W,imageHeight:H,inferenceTimeMs:typeof r?.inference_time_ms==="number"?r.inference_time_ms:0}}):[];const totalInferenceTimeMs=typeof data?.total_inference_time_ms==="number"?data.total_inference_time_ms:0;return {results,totalInferenceTimeMs,backend:"florence2"}}
const HEALTH_POLL_INTERVAL_MS=1e3;const HEALTH_POLL_TIMEOUT_MS=9e4;const HEALTH_FETCH_TIMEOUT_MS=2e3;const VENV_PACKAGES_COMMON=["fastapi==0.128.8","uvicorn[standard]==0.39.0","pillow==10.4.0","transformers==4.57.6","torch==2.8.0","einops==0.8.2","timm==1.0.26","tokenizers==0.22.2","sentencepiece==0.2.1","huggingface_hub==0.36.2"];const VENV_PACKAGES_MACOS=["mlx==0.29.3","mlx-lm==0.29.1","mlx-vlm==0.1.13","coremltools==9.0"];const VENV_PACKAGES=process.platform==="darwin"?[...VENV_PACKAGES_COMMON,...VENV_PACKAGES_MACOS]:VENV_PACKAGES_COMMON;let _activePort=null;function pluginRoot(){return process.cwd()}function venvDir(){return path.join(os.homedir(),".fastvlm","venv")}function venvPython(){return path.join(venvDir(),"bin","python3")}function venvPip(){return path.join(venvDir(),"bin","pip")}function venvExists(){return fs.existsSync(venvPython())}function venvPythonVersion(){try{const out=child_process.execFileSync(venvPython(),["--version"],{encoding:"utf-8",timeout:5e3});const m=out.trim().match(/^Python (\d+\.\d+)/);return m?m[1]:null}catch{return null}}function logsDir(){return path.join(pluginRoot(),"logs")}function pidFilePath(){return path.join(logsDir(),"fastvlm-server.pid")}function logFilePath(){return path.join(logsDir(),"mlx-vision-server.log")}function runAndStream(cmd,args,cwd,onLine,logFile){return new Promise((resolve,reject)=>{const child=child_process.spawn(cmd,args,{cwd,stdio:["ignore","pipe","pipe"]});const logStream=logFile?fs.createWriteStream(logFile,{flags:"a"}):null;function onData(data){for(const line of data.toString().split("\n")){const t=line.trim();if(t){onLine(t);logStream?.write(t+"\n");}}}child.stdout.on("data",onData);child.stderr.on("data",onData);child.on("error",err=>{logStream?.end();reject(err);});child.on("close",code=>{logStream?.end();if(code===0)resolve();else reject(new Error(`${path.basename(cmd)} exited with code ${code}`));});})}const PYTHON39="/Library/Developer/CommandLineTools/usr/bin/python3.9";async function ensureVenv(onStatus,logFile){if(venvExists()){const version=venvPythonVersion();if(version!==null&&!version.startsWith("3.9")){onStatus(`Python environment is ${version} — rebuilding with Python 3.9…`);fs.rmSync(venvDir(),{recursive:true,force:true});}}if(!venvExists()){onStatus("Creating Python environment…");await runAndStream(PYTHON39,["-m","venv",venvDir()],pluginRoot(),onStatus,logFile);onStatus("Installing dependencies — this may take a few minutes…");await runAndStream(venvPip(),["install",...VENV_PACKAGES],pluginRoot(),onStatus,logFile);}await ensureVenvPatches(onStatus,logFile);onStatus("Python environment ready");}async function ensureVenvPatches(onStatus,logFile){const libDir=path.join(venvDir(),"lib");const pythonSubdir=fs.existsSync(libDir)?fs.readdirSync(libDir).find(d=>d.startsWith("python"))??"python3.9":"python3.9";const sitePackages=path.join(venvDir(),"lib",pythonSubdir,"site-packages");const mlxVlmRoot=path.join(sitePackages,"mlx_vlm");const coremltoolsMarker=path.join(venvDir(),"coremltools_installed.marker");if(!fs.existsSync(coremltoolsMarker)){onStatus("Installing missing dependencies…");await runAndStream(venvPip(),["install","coremltools==9.0"],pluginRoot(),onStatus,logFile);fs.writeFileSync(coremltoolsMarker,new Date().toISOString());}const mlxVlmUtils=path.join(mlxVlmRoot,"utils.py");if(fs.existsSync(mlxVlmUtils)){let src=fs.readFileSync(mlxVlmUtils,"utf-8");src=src.replace(/MODEL_REMAPPING\s*=\s*\{([^}]*)\}/,match=>{if(match.includes("llava_qwen2"))return match;return match.replace(/\}$/,', "llava_qwen2": "fastvlm"}')});if(!src.includes("import coremltools")){src=src.replace(/^(import mlx\.nn as nn\s*\n)/m,"$1import coremltools\n");}if(!src.includes("hasattr(model_class, 'VisionModel')")){src=src.replace(/ weights = sanitize_weights\(\s*model_class\.VisionModel, weights, model_config\.vision_config\s*\)/,[" if hasattr(model_class, 'VisionModel'):"," weights = sanitize_weights("," model_class.VisionModel, weights, model_config.vision_config"," )"," else:"," # Load CoreML vision tower (used by fastvlm)",' print("Looking for CoreML vision tower")',' coreml_file = glob.glob(str(model_path / "*.mlpackage"))'," if len(coreml_file) > 0:",' assert len(coreml_file) == 1, "Found multiple vision model files."',' print(f"Loading {coreml_file[0]} vision tower")'," model.vision_tower = coremltools.models.MLModel(coreml_file[0], compute_units=coremltools.ComputeUnit.CPU_ONLY)"].join("\n"));}fs.writeFileSync(mlxVlmUtils,src,"utf-8");}const mlxVlmPromptUtils=path.join(mlxVlmRoot,"prompt_utils.py");if(fs.existsSync(mlxVlmPromptUtils)){let src=fs.readFileSync(mlxVlmPromptUtils,"utf-8");src=src.replace(/("llava":\s*"message_list_with_image",)(\s*"llava_next":)/,(match,llavaEntry,llavaNextEntry)=>{if(src.includes('"llava_qwen2"'))return match;return `${llavaEntry}
"llava_qwen2": "message_with_image_token_new_line",${llavaNextEntry}`});src=src.replace(/if "chat_template" in processor\.__dict__\.keys\(\):/,match=>{if(src.includes("processor.chat_template is not None"))return match;return `if ("chat_template" in processor.__dict__.keys()) and (processor.chat_template is not None):`});fs.writeFileSync(mlxVlmPromptUtils,src,"utf-8");}const fastvlmModelDest=path.join(mlxVlmRoot,"models","fastvlm");const fastvlmModelSrc=path.join(pluginRoot(),"src","fastvlm_server","mlx_vlm_patches","models","fastvlm");fs.mkdirSync(fastvlmModelDest,{recursive:true});for(const file of fs.readdirSync(fastvlmModelSrc)){fs.copyFileSync(path.join(fastvlmModelSrc,file),path.join(fastvlmModelDest,file));}}const PYTHON310="/opt/homebrew/bin/python3.10";function qwen3VlVenvDir(){return path.join(os.homedir(),".fastvlm","qwen3vl_venv")}function qwen3VlVenvPip(){return path.join(qwen3VlVenvDir(),"bin","pip")}function qwen3VlVenvExists(){return fs.existsSync(path.join(qwen3VlVenvDir(),"bin","python3"))}function qwen3VlReadyMarker(){return path.join(qwen3VlVenvDir(),"qwen3vl_ready.marker")}async function ensureQwen3VlVenv(onStatus,logFile){if(fs.existsSync(qwen3VlReadyMarker())){return}if(!qwen3VlVenvExists()){onStatus("Creating Qwen3-VL Python environment (Python 3.10)…");await runAndStream(PYTHON310,["-m","venv",qwen3VlVenvDir()],pluginRoot(),onStatus,logFile);}onStatus("Installing mlx-vlm for Qwen3-VL — this may take a few minutes…");await runAndStream(qwen3VlVenvPip(),["install","--upgrade","pip"],pluginRoot(),onStatus,logFile);await runAndStream(qwen3VlVenvPip(),["install","pillow","git+https://github.com/Blaizzy/mlx-vlm.git"],pluginRoot(),onStatus,logFile);fs.writeFileSync(qwen3VlReadyMarker(),new Date().toISOString());onStatus("Qwen3-VL environment ready");}function readPid(){try{const raw=fs.readFileSync(pidFilePath(),"utf-8").trim();const n=parseInt(raw,10);return Number.isFinite(n)&&n>0?n:null}catch{return null}}function isProcessAlive(pid){try{process.kill(pid,0);return true}catch{return false}}function fetchWithTimeout(url,options,timeoutMs){const controller=new AbortController;const timer=setTimeout(()=>controller.abort(),timeoutMs);return fetch(url,{...options,signal:controller.signal}).finally(()=>clearTimeout(timer))}async function pollHealth(port,onAttempt){const url=`http://127.0.0.1:${port}/health`;const deadline=Date.now()+HEALTH_POLL_TIMEOUT_MS;let attempt=0;while(Date.now()<deadline){attempt++;onAttempt(attempt);try{const res=await fetchWithTimeout(url,{},HEALTH_FETCH_TIMEOUT_MS);if(res.ok)return true}catch{}await new Promise(r=>setTimeout(r,HEALTH_POLL_INTERVAL_MS));}return false}async function ensureFastvlmServerRunning(config,onStatus){const{port}=config;_activePort=port;const requestedBackend=config.backend??"mlx";const requestedDetectBackend=config.detectBackend??"florence2";try{const res=await fetchWithTimeout(`http://127.0.0.1:${port}/health`,{},HEALTH_FETCH_TIMEOUT_MS);if(res.ok){let runningBackend=null;let runningDetectBackend=null;try{const statusRes=await fetchWithTimeout(`http://127.0.0.1:${port}/status`,{},HEALTH_FETCH_TIMEOUT_MS);if(statusRes.ok){const statusJson=await statusRes.json();runningBackend=statusJson.backend??null;runningDetectBackend=statusJson.detect_backend??null;}}catch{}const backendMismatch=runningBackend!==null&&runningBackend!==requestedBackend;const detectBackendMismatch=runningDetectBackend!==null&&runningDetectBackend!==requestedDetectBackend;if(backendMismatch||detectBackendMismatch){const isoNow=()=>new Date().toISOString().replace(/\.\d+Z$/,"Z");fs.appendFileSync(logFilePath(),`[mgr] ${isoNow()} Backend mismatch: running=${runningBackend}/${runningDetectBackend}, requested=${requestedBackend}/${requestedDetectBackend} — restarting [${port}]
`);onStatus("Restarting server (backend changed)…");await stopFastvlmServer(port);}else {if(requestedDetectBackend==="qwen3-vl"){const logFile=logFilePath();fs.mkdirSync(logsDir(),{recursive:true});await ensureQwen3VlVenv(onStatus,logFile);}const pid=readPid();fs.appendFileSync(logFilePath(),`[mgr] ${new Date().toISOString().replace(/\.\d+Z$/,"Z")} Adopted [${port}]${pid!==null?` — PID ${pid}`:""}
`);return}}}catch{}const stalePid=readPid();if(stalePid!==null&&isProcessAlive(stalePid)){try{process.kill(stalePid,"SIGTERM");}catch{}}fs.mkdirSync(logsDir(),{recursive:true});const logFile=logFilePath();const effectiveConfig=process.platform!=="darwin"?{...config,mlxVisionEnabled:false}:config;await ensureVenv(onStatus,logFile);if(effectiveConfig.detectBackend==="qwen3-vl"){await ensureQwen3VlVenv(onStatus,logFile);}const args=["-m","fastvlm_server","--port",String(port),"--host","127.0.0.1","--pid-file",pidFilePath()];if(effectiveConfig.mlxVisionEnabled&&effectiveConfig.modelPath.trim()){args.push("--model",effectiveConfig.modelPath.trim());}const backend=effectiveConfig.backend??"mlx";args.push("--backend",backend);if(effectiveConfig.maxTokens!==undefined){args.push("--max-tokens",String(effectiveConfig.maxTokens));}if(effectiveConfig.temperature!==undefined){args.push("--temperature",String(effectiveConfig.temperature));}if(effectiveConfig.florence2ModelPath?.trim()){args.push("--florence2-model-path",effectiveConfig.florence2ModelPath.trim());}if(effectiveConfig.detectBackend){args.push("--detect-backend",effectiveConfig.detectBackend);}if(effectiveConfig.qwen3VlModelPath?.trim()){args.push("--qwen3-vl-model-path",effectiveConfig.qwen3VlModelPath.trim());}args.push("--lazy");args.push("--log-file",logFile);onStatus("Starting server…");const pythonPath=path.join(pluginRoot(),"src");const isoNow=()=>new Date().toISOString().replace(/\.\d+Z$/,"Z");fs.appendFileSync(logFile,`[mgr] ${isoNow()} Starting [${port}] model=${effectiveConfig.modelPath.trim()||"(none)"}
`);const child=child_process.spawn(venvPython(),args,{cwd:pluginRoot(),detached:true,stdio:"ignore",env:{...process.env,PYTHONPATH:pythonPath}});child.unref();fs.appendFileSync(logFile,`[mgr] ${isoNow()} Spawned [${port}] via double-fork
`);const ready=await pollHealth(port,n=>{onStatus("Loading…");});if(!ready){throw new Error(`FastVLM server did not become healthy within ${HEALTH_POLL_TIMEOUT_MS/1e3}s. `+`Check logs: ${logFile}`)}const startedPid=readPid();fs.appendFileSync(logFile,`[mgr] ${isoNow()} Started [${port}]${startedPid!==null?` — PID ${startedPid}`:""}
`);onStatus("Server ready");}async function stopFastvlmServer(port){const logFile=logFilePath();const isoNow=()=>new Date().toISOString().replace(/\.\d+Z$/,"Z");const pid=readPid();if(pid!==null&&isProcessAlive(pid)){fs.appendFileSync(logFile,`[mgr] ${isoNow()} Stopping [${port}]...
`);try{await fetchWithTimeout(`http://127.0.0.1:${port}/shutdown`,{method:"POST"},3e3);}catch{try{process.kill(pid,"SIGTERM");}catch{}}fs.appendFileSync(logFile,`[mgr] ${isoNow()} Stopped [${port}]
`);}try{fs.unlinkSync(pidFilePath());}catch{}}async function stopActiveFastvlmServer(){await stopFastvlmServer(_activePort??8765);}
function formatPluginMeta$1(){try{const cwd=process.cwd();const pkg=JSON.parse(fs.readFileSync(path.join(cwd,"package.json"),"utf-8"));const mf=JSON.parse(fs.readFileSync(path.join(cwd,"manifest.json"),"utf-8"));const id=mf?.owner&&mf?.name?`${mf.owner}/${mf.name}`:pkg?.name||"ceveyne/analyse-image";return `Plugin-Identifier: ${id}
Plugin version: ${pkg?.version||""}`}catch{return "Plugin-Identifier: ceveyne/analyse-image"}}const FlexibleTargetsList$1=zod.z.union([zod.z.string().transform(s=>s.split(/[\s,]+/).map(x=>x.trim()).filter(x=>x.length>0)),zod.z.array(zod.z.string())]).refine(arr=>arr.length>=1,"targets must contain at least one notation").refine(arr=>arr.length<=16,"targets must contain at most 16 notations (MLX API limit)");const AnalyseImageParamsShape={targets:FlexibleTargetsList$1,prompt:zod.z.string().optional().describe("Optional prompt for the vision model. Empty = model default.")};function parseTargets(targets){const parsed={a:[],v:[],i:[],p:[]};const invalid=[];for(const raw of targets){const s=typeof raw==="string"?raw.trim():"";const m=/^([avip])(\d+)$/i.exec(s);if(!m){invalid.push(String(raw));continue}const kind=m[1].toLowerCase();const n=parseInt(m[2],10);if(!Number.isFinite(n)||n<=0){invalid.push(String(raw));continue}parsed[kind].push(n);}Object.keys(parsed).forEach(k=>{parsed[k]=Array.from(new Set(parsed[k])).sort((a,b)=>a-b);});return {parsed,invalid}}function getAvailable(state){const availableA=(state.attachments||[]).map(x=>typeof x?.a==="number"?x.a:undefined).filter(x=>typeof x==="number"&&x>0).sort((a,b)=>a-b);const availableV=(state.variants||[]).map(x=>typeof x?.v==="number"?x.v:undefined).filter(x=>typeof x==="number"&&x>0).sort((a,b)=>a-b);const availableI=(state.images||[]).map(x=>typeof x?.i==="number"?x.i:undefined).filter(x=>typeof x==="number"&&x>0).sort((a,b)=>a-b);const availableP=(state.pictures||[]).map(x=>typeof x?.p==="number"?x.p:undefined).filter(x=>typeof x==="number"&&x>0).sort((a,b)=>a-b);return {availableA,availableV,availableI,availableP}}async function ensurePreviewExists(chatWd,previewRel){const pAbs=path.join(chatWd,previewRel);await fs.promises.access(pAbs,fs.constants.F_OK);}function classifyVisionError(errMsg){if(/\b503\b/.test(errMsg)){return "The FastVLM server is running, but no vision model is currently loaded. Load a model in LM Studio and try again."}if(/ECONNREFUSED|ENOTFOUND|ECONNRESET|network socket|fetch failed/i.test(errMsg)){return "The FastVLM server is not reachable. Make sure it is running (default: http://localhost:8765) and try again."}return `Vision API error: ${errMsg}`}function createAnalyseImageTool(ctl){return sdk.tool({name:"analyse_image",description:`Analyze existing media items (images, pictures, variants, attachments) using a vision model.
Returns text descriptions for each requested item.
Use this when the user asks you to describe, analyze, or understand image content.
Also use this to retrieve generation parameters (prompt, model, sampler, seed, steps, guidance scale, LoRA, source images, …) for images generated by Draw Things. Those parameters are embedded in the PNG file and are automatically included in the result alongside the visual analysis. Call this tool whenever the user asks about how an image was generated, what settings were used, or wants to reuse the generation parameters.
Parameters:
- targets: Array of explicit notations: 'aN' (attachment), 'vN' (variant), 'iN' (image), 'pN' (picture)
- prompt: Optional vision prompt (empty = model default) — MUST be written in English
${formatPluginMeta$1()}`,parameters:AnalyseImageParamsShape,implementation:async(args,ctx)=>{try{const strict=false;const targets=Array.isArray(args?.targets)?args.targets:[];const prompt=typeof args?.prompt==="string"?args.prompt:"";const{parsed,invalid:invalidRaw}=parseTargets(targets);const workingDir=ctl.getWorkingDirectory();if(typeof workingDir!=="string"||!workingDir.trim()){return "analyse_image failed: working directory not available."}const chatWd=workingDir;try{await syncAttachmentsToState(chatWd,false,Number.MAX_SAFE_INTEGER);}catch(syncErr){console.warn("[analyse_image] attachment sync failed (non-fatal):",syncErr?.message??syncErr);}const state=await readState$1(chatWd);const{availableA,availableV,availableI,availableP}=getAvailable(state);if(invalidRaw.length>0&&strict);const analysisItems=[];const originalFilePaths=new Map;const displayNames=new Map;const missingNotations=new Set;const missingDetails=[];const addItem=async(notation,rec,previewField,originalAbsPath,displayName)=>{if(!rec){missingNotations.add(notation);missingDetails.push(notation);return false}const previewRel=typeof rec[previewField]==="string"?String(rec[previewField]):"";if(!previewRel.trim()){missingNotations.add(notation);missingDetails.push(`${notation} (missing preview)`);return false}try{await ensurePreviewExists(chatWd,previewRel);analysisItems.push({id:notation,filePath:path.join(chatWd,previewRel)});if(originalAbsPath){originalFilePaths.set(notation,originalAbsPath);}const dn=displayName||(originalAbsPath?path.basename(originalAbsPath):undefined);if(dn){displayNames.set(notation,dn);}return true}catch{missingNotations.add(notation);missingDetails.push(`${notation} (preview file missing)`);return false}};for(const n of parsed.a){const rec=(state.attachments||[]).find(x=>x?.a===n);const origAbs=rec?.originAbs??(rec?.filename?path.join(chatWd,rec.filename):undefined);const origName=typeof rec?.originalName==="string"&&rec.originalName?rec.originalName:undefined;await addItem(`a${n}`,rec,"preview",origAbs,origName);}for(const n of parsed.v){const rec=(state.variants||[]).find(x=>x?.v===n);const origAbs=rec?.filename?path.join(chatWd,rec.filename):undefined;await addItem(`v${n}`,rec,"preview",origAbs);}for(const n of parsed.i){const rec=(state.images||[]).find(x=>x?.i===n);const origAbs=rec?.filename?path.join(chatWd,rec.filename):undefined;await addItem(`i${n}`,rec,"preview",origAbs);}for(const n of parsed.p){const rec=(state.pictures||[]).find(x=>x?.p===n);const origAbs=rec?.filename?path.join(chatWd,rec.filename):undefined;await addItem(`p${n}`,rec,"preview",origAbs);}if(missingDetails.length>0&&strict);if(analysisItems.length===0){const hint=`Available: `+`a=[${availableA.map(x=>`a${x}`).join(", ")||"(none)"}] `+`v=[${availableV.map(x=>`v${x}`).join(", ")||"(none)"}] `+`i=[${availableI.map(x=>`i${x}`).join(", ")||"(none)"}] `+`p=[${availableP.map(x=>`p${x}`).join(", ")||"(none)"}]`;return `analyse_image: no valid targets found. ${hint}`}const serverTTL=parseInt(process.env.SERVER_TTL??"1440",10);if(serverTTL!==0){try{await ensureFastvlmServerRunning({port:parseInt(process.env.MLX_VISION_PORT??"8765",10),modelPath:process.env.MLX_VISION_MODEL_PATH??"",mlxVisionEnabled:process.env.MLX_VISION_ENABLED!=="false",backend:process.env.FASTVLM_BACKEND||"mlx",maxTokens:process.env.MLX_VISION_MAX_TOKENS?parseInt(process.env.MLX_VISION_MAX_TOKENS,10):undefined,temperature:process.env.MLX_VISION_TEMPERATURE?parseFloat(process.env.MLX_VISION_TEMPERATURE):undefined,florence2ModelPath:process.env.FLORENCE2_MODEL_PATH||process.env.DETECT_MODEL_PATH||""},msg=>{try{ctx.status(msg);}catch{}});}catch(e){throw new Error(`Failed to start FastVLM server: ${e.message||String(e)}`)}}try{ctx.status(`Analyzing ${analysisItems.length} image${analysisItems.length>1?"s":""}...`);}catch{}const port=parseInt(process.env.MLX_VISION_PORT??"8765",10);const mlxConfig={endpoint:process.env.MLX_VISION_ENDPOINT||`http://localhost:${port}/analyze`,prompt:prompt||process.env.MLX_VISION_PROMPT||undefined,timeoutMs:3e4};let visionError=null;const visionResults=new Map;let totalInferenceTimeMs=null;try{const batchResult=await analyzeMlxVisionBatch(analysisItems,mlxConfig);for(const r of batchResult.results){visionResults.set(r.id,r.text.trim()||"(no description)");}totalInferenceTimeMs=batchResult.totalInferenceTimeMs;}catch(e){visionError=classifyVisionError(e.message||String(e));}const includeGenMeta=process.env.INCLUDE_GENERATION_METADATA!=="false";const lines=[];lines.push(`Analysis results (${analysisItems.length} image${analysisItems.length!==1?"s":""}):`);lines.push("");if(visionError){lines.push(`Note: Visual analysis unavailable — ${visionError}`);lines.push("");}for(const item of analysisItems){const{id}=item;const displayName=displayNames.get(id);const origPath=originalFilePaths.get(id);const header=displayName?`${id} — ${displayName}`:id;lines.push(`- ${header}`);if(visionError){lines.push(` Visual: (not available)`);}else {lines.push(` Visual: ${visionResults.get(id)??"(no description)"}`);}if(includeGenMeta){if(origPath&&origPath.toLowerCase().endsWith(".png")){const meta=readPngGenerationMeta(origPath);if(meta){lines.push(formatGenerationMeta(meta));}else {lines.push(` (No embedded generation metadata)`);}}else if(origPath){lines.push(` (No embedded generation metadata — not a PNG file)`);}}lines.push("");}if(totalInferenceTimeMs!==null){lines.push(`Total inference time: ${Math.round(totalInferenceTimeMs)}ms`);}try{const statusSuffix=visionError?" (vision unavailable)":" successfully";ctx.status(`Analyzed ${analysisItems.length} image${analysisItems.length!==1?"s":""}${statusSuffix}`);}catch{}return lines.join("\n")}catch(e){return `analyse_image failed: ${String(e?.message||e)}`}}})}
const globalConfigSchematics=sdk.createConfigSchematics().field("PREVIEW_IN_CHAT","boolean",{displayName:"Previews in Chat",subtitle:"When enabled, tool responses include inline image previews. Recommended for local models without vision capability."},true).field("mlxVisionEnabled","boolean",{displayName:"MLX Vision: Load Model",subtitle:"When enabled, the FastVLM model is loaded on server start. Disable if you do not use vision analysis."},true).field("mlxVisionBackend","boolean",{displayName:"FastVLM: CoreML Vision Backend",subtitle:"Off (default): MLX Metal GPU. On: CoreML CPU inference for vision — frees Metal GPU for language generation (requires fastvithd.mlpackage).",engineDoesNotSupport:true},false).field("mlxVisionEndpoint","string",{displayName:"MLX Vision Endpoint",subtitle:"URL of the MLX Vision /analyze endpoint. Default: http://localhost:8765/analyze",placeholder:"http://localhost:8765/analyze",engineDoesNotSupport:true},"http://localhost:8765/analyze").field("mlxVisionPrompt","string",{displayName:"Vision Prompt",subtitle:"Default prompt sent to the vision model. Leave empty to use the model default.",placeholder:"",isParagraph:true},["Analyze this image based strictly on what is directly visible. Do not infer, assume, or complete information that is not present.","","STEP 1 — GROUND TRUTH (always required):",'Before any detailed analysis, state in one sentence what the image actually shows (e.g., "This image shows a person", "This image shows a geometric shape", "This image shows a product on a plain background"). If the image does not contain a person, skip all person-specific sections below and describe only what is present.',"","---","","IF AND ONLY IF a person is visible, describe:","","1. SUBJECT PHYSICAL CHARACTERISTICS:"," - Face shape, skin tone, facial features (eyes, nose, lips, eyebrows) — only what is clearly visible"," - Hair: color, length, style, texture, specific arrangement"," - Visible age indicators and gender markers based on physical traits alone","","2. CLOTHING & DESIGN ELEMENTS — only if clothing is present:"," - Garment type, colors, patterns, textures (only if present and visible)"," - Specific design features (lines, shapes, geometric elements)"," - Color palette with exact color names where possible","","3. COMPOSITION & FRAMING:"," - What is included in the frame (head position, body coverage)"," - Background characteristics and spatial relationships"," - Lighting quality, direction, shadow patterns","","4. CULTURAL & ETHNIC INDICATORS:"," - Specific facial features that suggest ethnic background"," - Any visible cultural markers in clothing or styling"," - Note only what is visually present, not inferred","","---","","IF no person is visible, describe only:","- Shape, form, color, texture, and spatial relationships of what is actually present","- Composition and framing as above","","---","","INTERPRETATIONS (separate section, always):","- Based solely on the observable facts above, note any stylistic intentions or design approaches suggested by the visual evidence","- Clearly distinguish between what IS seen and what CAN BE INFERRED","",'Avoid vague terms like "beautiful," "modern," "gender-neutral" unless supported by specific visual evidence. Never describe content that is not present in the image.'].join("\n")).field("detectEndpoint","string",{displayName:"Florence-2 Detect Endpoint",subtitle:"URL of the Florence-2 /detect endpoint. Default: http://localhost:8765/detect",placeholder:"http://localhost:8765/detect",engineDoesNotSupport:true},"http://localhost:8765/detect").field("mlxVisionModelPath","string",{displayName:"MLX Vision: Model Path",subtitle:"Absolute path to the FastVLM model directory (e.g. FastVLM-7B-int4). Required for Node-managed server mode.",placeholder:"~/Documents/Models/FastVLM-7B-MLX"},"").field("mlxVisionPort","numeric",{displayName:"MLX Vision: Port",subtitle:"Port for the local FastVLM server (shared with Florence-2 detect). Default: 8765."},8765).field("mlxVisionMaxTokens","numeric",{displayName:"MLX Vision: Max Tokens",subtitle:"Maximum response length in tokens (1–4096). Default: 384."},384).field("mlxVisionTemperature","numeric",{displayName:"MLX Vision: Temperature",subtitle:"Sampling temperature (0.0–2.0). Default: 0.7."},.7).field("detectEnabled","boolean",{displayName:"Detection: Load Model",subtitle:"When enabled, the detection model is loaded on server start. Disable if object detection is not needed."},true).field("detectModelPath","string",{displayName:"Florence-2: Model Path",subtitle:"Absolute path to the Florence-2 model directory. Required for Node-managed server mode.",placeholder:"~/Documents/Models/Florence-2-large"},"").field("detectBackend","boolean",{displayName:"Detection Backend: Use Qwen3-VL",subtitle:"When enabled, Qwen3-VL is used for object detection instead of Florence-2. Requires Qwen3-VL Model Path below.",engineDoesNotSupport:false},false).field("qwen3VlOdPrompt","string",{displayName:"Qwen3-VL: Object Detection Prompt",subtitle:"Instruction sent to Qwen3-VL for default object detection (task omitted or '<OD>'). Leave empty to use the built-in default.",placeholder:"",isParagraph:true,engineDoesNotSupport:false},["Detect objects in the image with strict hierarchical prioritization.","","PRIORITY 1 (CRITICAL - MUST DETECT FIRST):",'- You MUST detect "human face" (highest priority if a person is present)','- You MUST detect "person" (if no face is clearly visible or if the person is the main subject)',"","PRIORITY 2 (MAIN SUBJECT / HERO ELEMENT):","- The most visually prominent object or subject that is NOT part of the background.","- Use specific, concrete labels (e.g., 'red car', 'fluffy owl toy').","- Avoid generic terms like 'object' or 'thing'.","","PRIORITY 3 (CONTEXTUAL BACKGROUND ELEMENTS):","- Only detect background elements if they are significant to the scene composition OR if the main subject is interacting with them.","- Do not detect minor or redundant background details.","","PRIORITY 4 (FOCUSSED MAIN SUBJECT / HERO ELEMENT):","- All visible body parts (hands, feet, arms, legs).","- Elements of the face, as far as clearly detectable and focussed on close-ups: nose, mouth, left and right eyes, eyebrows and ears","- anatomical details, as far as recognizable as \\\"focussed\\\" or \\\"prominent\\\" (e.g., 'iris', 'pupil', 'eyelid')","","RULES:","- Maximum 16 objects total.","- Each bounding box must be unique and non-redundant.","- For clothing, name the specific garment (e.g., 'tank top', 'jeans').","- For body parts, qualify by position (e.g., 'left hand').","- NEVER prioritize background elements over the main subject or human face.","- NEVER prioritize anatomical details over general concepts unless they are solely focussed (e.g. only detect 'eyes' unless 'human face' is the dominant part of the image)","- If the main subject is a person, focus on the person and their immediate interactions. Ignore background elements unless they are directly involved in the interaction."].join("\n")).field("qwen3VlModelPath","string",{displayName:"Qwen3-VL: Model Path",subtitle:"Absolute path to the Qwen3-VL MLX model directory (e.g. Qwen3-VL-8B-Instruct-MLX-4bit). Required when Detection Backend is set to Qwen3-VL.",placeholder:"~/.lmstudio/models/lmstudio-community/Qwen3-VL-8B-Instruct-MLX-4bit",engineDoesNotSupport:false},"").field("serverTTL","numeric",{displayName:"Server TTL (minutes)",subtitle:"Controls server lifetime. 0 = do not start server; N = offload model after N minutes of inactivity; 1440 = keep loaded until LM Studio exits.",engineDoesNotSupport:true},1440).field("HTTP_SERVER_PORT","numeric",{displayName:"Local HTTP Server Port",subtitle:"Port for serving generated images over localhost (default: 54760).",engineDoesNotSupport:true},54760).field("includeGenerationMetadata","boolean",{displayName:"Include Generation Metadata",subtitle:"When enabled, Draw Things generation parameters (prompt, model, sampler, seed, …) embedded in PNG files are appended to each analysis result.",engineDoesNotSupport:false},true).build();const FLORENCE2_MODEL_PATH=process.env["FLORENCE2_MODEL_PATH"]??"";const DETECT_ENDPOINT=process.env["DETECT_ENDPOINT"]??"http://localhost:8765/detect";
async function ensureDetectServerRunning(config,onStatus){const shared={port:config.port,modelPath:config.mlxVisionModelPath,mlxVisionEnabled:config.mlxVisionEnabled,florence2ModelPath:config.florence2ModelPath,backend:config.backend,maxTokens:config.maxTokens,temperature:config.temperature,detectBackend:config.detectBackend,qwen3VlModelPath:config.qwen3VlModelPath};await ensureFastvlmServerRunning(shared,onStatus);}
async function drawBboxesOnImage(buffer,bboxes,sourceDims){const _require=typeof require!=="undefined"?require:(await import('module')).createRequire(__filename);const jimpMod=_require("jimp");const Jimp=jimpMod.Jimp??jimpMod.default??jimpMod;if(!Jimp||typeof Jimp.read!=="function"){throw new Error("drawBboxesOnImage: Jimp.read not available")}const img=await Jimp.read(buffer);const imgW=typeof img.getWidth==="function"?img.getWidth():typeof img.width==="number"?img.width:img.bitmap?.width||0;const imgH=typeof img.getHeight==="function"?img.getHeight():typeof img.height==="number"?img.height:img.bitmap?.height||0;const scaleX=sourceDims&&sourceDims.width>0?imgW/sourceDims.width:1;const scaleY=sourceDims&&sourceDims.height>0?imgH/sourceDims.height:1;const palette=[[255,59,48,255],[52,199,89,255],[0,122,255,255],[255,159,10,255],[191,90,242,255],[255,214,10,255]];const thickness=2;for(let bi=0;bi<bboxes.length;bi++){const[r,g,b,a]=palette[bi%palette.length];const colorInt=((r&255)<<24|(g&255)<<16|(b&255)<<8|a&255)>>>0;const[bx1,by1,bx2,by2]=bboxes[bi];const x1raw=bx1*scaleX;const y1raw=by1*scaleY;const x2raw=bx2*scaleX;const y2raw=by2*scaleY;const x1=Math.max(0,Math.min(imgW-1,Math.round(x1raw)));const y1=Math.max(0,Math.min(imgH-1,Math.round(y1raw)));const x2=Math.max(0,Math.min(imgW-1,Math.round(x2raw)));const y2=Math.max(0,Math.min(imgH-1,Math.round(y2raw)));for(let t=0;t<thickness;t++){for(let x=x1;x<=x2;x++){if(y1+t<imgH)img.setPixelColor(colorInt,x,y1+t);if(y2-t>=0)img.setPixelColor(colorInt,x,y2-t);}for(let y=y1;y<=y2;y++){if(x1+t<imgW)img.setPixelColor(colorInt,x1+t,y);if(x2-t>=0)img.setPixelColor(colorInt,x2-t,y);}}}const bufResult=typeof img.getBufferAsync==="function"?img.getBufferAsync("image/png"):img.getBuffer("image/png");if(bufResult&&typeof bufResult.then==="function"){return bufResult}return new Promise((resolve,reject)=>img.getBuffer("image/png",(err,data)=>err?reject(err):resolve(data)))}function isoStampCompact(){const d=new Date;const year=d.getUTCFullYear();const month=String(d.getUTCMonth()+1).padStart(2,"0");const day=String(d.getUTCDate()).padStart(2,"0");const hours=String(d.getUTCHours()).padStart(2,"0");const minutes=String(d.getUTCMinutes()).padStart(2,"0");const seconds=String(d.getUTCSeconds()).padStart(2,"0");const millis=String(d.getUTCMilliseconds()).padStart(3,"0");return `${year}${month}${day}T${hours}${minutes}${seconds}${millis}Z`}function parsePrefixedNotation(s){const t=String(s||"").trim().toLowerCase();const m=t.match(/^([avip])(\d+)$/);if(!m)return null;const idx=Math.max(1,parseInt(m[2],10));const pool=m[1]==="a"?"attachment":m[1]==="v"?"variant":m[1]==="i"?"image":"picture";return {pool,index:idx}}function formatPluginMeta(){try{const cwd=process.cwd();const pkg=JSON.parse(fs.readFileSync(path.join(cwd,"package.json"),"utf-8"));const mf=JSON.parse(fs.readFileSync(path.join(cwd,"manifest.json"),"utf-8"));const id=mf?.owner&&mf?.name?`${mf.owner}/${mf.name}`:pkg?.name||"ceveyne/analyse-image";return `Plugin-Identifier: ${id}
Plugin version: ${pkg?.version||""}`}catch{return "Plugin-Identifier: ceveyne/analyse-image"}}const FlexibleTargetsList=zod.z.union([zod.z.string().transform(s=>s.split(/[\s,]+/).map(x=>x.trim()).filter(x=>x.length>0)),zod.z.array(zod.z.string())]).refine(arr=>arr.length>=1,"targets must contain at least one notation").refine(arr=>arr.length<=16,"targets must contain at most 16 notations");const DetectObjectParamsShape={targets:FlexibleTargetsList.optional().describe("One or more source image notations (e.g. ['a1','i3'] or 'a1, i3'). "+"Omit when there is exactly one image — it will be selected automatically."),task:zod.z.string().optional().default("<OD>").describe("Florence-2 detection task token. Default: '<OD>' (object detection).")};function createDetectObjectTool(ctl){return sdk.tool({name:"detect_object",description:`Detect objects in one or more images and draw colored bounding boxes on each result.
For each source image, returns a new annotated image (saved as iN) with bounding boxes for each detected object, plus a JSON summary with labels, coordinates, and crop percentages. The active backend is configured in plugin settings (Florence-2 or Qwen3-VL).
Parameters:
- targets: One or more image notations (e.g. ['a1','i3'] or 'a1, i3'). Accepts array or comma-separated string. Omit when there is exactly one image.
- task: What to detect.
- Florence-2 backend — use task tokens: '<OD>' (generic objects with labels), '<DENSE_REGION_CAPTION>' (richer captions per region), '<REGION_PROPOSAL>' (regions without labels), '<OPEN_VOCABULARY_DETECTION>dog' (specific concept, replace 'dog').
- Qwen3-VL backend — use natural language: e.g. 'Detect all faces and hands' or 'Find the cat and the laptop'. Omit for full-image general detection.
${formatPluginMeta()}`,parameters:DetectObjectParamsShape,implementation:async(args,ctx)=>{try{let rawTargets=[];if(Array.isArray(args?.targets)){rawTargets=args.targets.map(s=>String(s).trim()).filter(Boolean);}else if(typeof args?.targets==="string"&&args.targets.trim()){rawTargets=args.targets.trim().split(/[\s,]+/).map(s=>s.trim()).filter(Boolean);}const task=typeof args?.task==="string"&&args.task.trim()?args.task.trim():"<OD>";console.log("[detect_object] invoked",{targets:rawTargets,task});let currentLmChatId=null;let currentLmWorkingDir=null;try{const chatCtx=await getActiveChatContext();if(chatCtx?.chatId)currentLmChatId=chatCtx.chatId;if(chatCtx?.workingDir)currentLmWorkingDir=chatCtx.workingDir;}catch{}if(!currentLmChatId){try{const resolved=await resolveActiveLMStudioChatId();if(resolved?.ok)currentLmChatId=resolved.chatId;}catch{}}const primaryOutDir=currentLmWorkingDir||(currentLmChatId?getLMStudioWorkingDir(currentLmChatId):undefined);if(!primaryOutDir){console.error("[detect_object] could not resolve working directory");return {content:[{type:"text",text:"detect_object failed: could not resolve LM Studio chat working directory."}],isError:true}}console.log("[detect_object] primaryOutDir:",primaryOutDir);await fs.promises.mkdir(primaryOutDir,{recursive:true}).catch(()=>{});console.log("[detect_object] syncing attachments...");try{await syncAttachmentsToState(primaryOutDir,false,Number.MAX_SAFE_INTEGER);}catch(e){console.warn("[detect_object] attachment sync failed (non-fatal):",e?.message??e);}console.log("[detect_object] attachment sync done");console.log("[detect_object] reading state...");const st=await readState(primaryOutDir);const attachments=Array.isArray(st?.attachments)?st.attachments:[];const pictures=Array.isArray(st?.pictures)?st.pictures:[];const imageRecords=Array.isArray(st?.images)?st.images:[];const images=imageRecords.filter(r=>r&&typeof r.filename==="string").sort((a,b)=>(a.i||0)-(b.i||0)).map(r=>({i:r.i||1,path:path.join(primaryOutDir,r.filename)}));const variantRecords=Array.isArray(st?.variants)?st.variants:[];const variants=variantRecords.filter(v=>v&&typeof v.filename==="string").map(v=>({v:v.v||1,path:path.join(primaryOutDir,v.filename)}));console.log("[detect_object] state:",{attachments:attachments.length,images:images.length,variants:variants.length,pictures:pictures.length});const sourceEntries=[];async function resolveOneBuf(rawCanvas){const pref=parsePrefixedNotation(rawCanvas);if(!pref)throw new Error(`Invalid canvas notation: ${rawCanvas}`);if(pref.pool==="attachment"){const rec=attachments.find(a=>a?.a===pref.index);const previewRel=rec&&typeof rec.preview==="string"?rec.preview:"";if(!previewRel)throw new Error(`Preview for attachment a${pref.index} not found.`);return fs.promises.readFile(path.join(primaryOutDir,previewRel))}else if(pref.pool==="image"){const rec=imageRecords.find(r=>r?.i===pref.index);const previewRel=rec&&typeof rec.preview==="string"?rec.preview:"";if(!previewRel)throw new Error(`Preview for image i${pref.index} not found.`);return fs.promises.readFile(path.join(primaryOutDir,previewRel))}else if(pref.pool==="variant"){const rec=variantRecords.find(v=>v?.v===pref.index);const previewRel=rec&&typeof rec.preview==="string"?rec.preview:"";if(!previewRel)throw new Error(`Preview for variant v${pref.index} not found.`);return fs.promises.readFile(path.join(primaryOutDir,previewRel))}else {const rec=pictures.find(p=>p?.p===pref.index);const previewRel=rec&&typeof rec.preview==="string"?rec.preview:"";if(!previewRel)throw new Error(`Preview for picture p${pref.index} not found.`);return fs.promises.readFile(path.join(primaryOutDir,previewRel))}}async function resolveOriginalBuf(rawCanvas,previewFallback){try{const pref=parsePrefixedNotation(rawCanvas);if(!pref)return previewFallback;if(pref.pool==="attachment"){const rec=attachments.find(a=>a?.a===pref.index);const originAbs=rec&&typeof rec.originAbs==="string"?rec.originAbs:"";if(!originAbs)return previewFallback;return await fs.promises.readFile(originAbs)}let rec;if(pref.pool==="image")rec=imageRecords.find(r=>r?.i===pref.index);else if(pref.pool==="variant")rec=variantRecords.find(v=>v?.v===pref.index);else rec=pictures.find(p=>p?.p===pref.index);const filename=rec&&typeof rec.filename==="string"?rec.filename:"";if(!filename)return previewFallback;return await fs.promises.readFile(path.join(primaryOutDir,filename))}catch{return previewFallback}}try{if(rawTargets.length>0){for(const t of rawTargets){const buf=await resolveOneBuf(t);const origBuf=await resolveOriginalBuf(t,buf);sourceEntries.push({id:t,buf,origBuf});}}else {const total=attachments.length+variantRecords.length+imageRecords.length+pictures.length;if(total===0)throw new Error("No source image available.");if(total>1)throw new Error("Ambiguous source — specify targets explicitly.");let buf;let id;if(attachments.length===1){const rec=attachments[0];const previewRel=rec&&typeof rec.preview==="string"?rec.preview:"";if(!previewRel)throw new Error("Preview for attachment not found.");buf=await fs.promises.readFile(path.join(primaryOutDir,previewRel));id=`a${typeof rec.a==="number"?rec.a:1}`;}else if(variantRecords.length===1){const rec=variantRecords[0];const previewRel=rec&&typeof rec.preview==="string"?rec.preview:"";if(!previewRel)throw new Error("Preview for variant not found.");buf=await fs.promises.readFile(path.join(primaryOutDir,previewRel));id=`v${typeof rec.v==="number"?rec.v:1}`;}else if(imageRecords.length===1){const rec=imageRecords[0];const previewRel=rec&&typeof rec.preview==="string"?rec.preview:"";if(!previewRel)throw new Error("Preview for image not found.");buf=await fs.promises.readFile(path.join(primaryOutDir,previewRel));id=`i${typeof rec.i==="number"?rec.i:1}`;}else {const rec=pictures[0];const previewRel=rec&&typeof rec.preview==="string"?rec.preview:"";if(!previewRel)throw new Error("Preview for picture not found.");buf=await fs.promises.readFile(path.join(primaryOutDir,previewRel));id=`p${rec.p??1}`;}const origBuf=await resolveOriginalBuf(id,buf);sourceEntries.push({id,buf,origBuf});}}catch(e){return {content:[{type:"text",text:String(e?.message||e)}],isError:true}}console.log("[detect_object] sources resolved:",sourceEntries.map(s=>s.id));const serverTTL=parseInt(process.env.SERVER_TTL??"1440",10);if(serverTTL!==0){try{await ensureDetectServerRunning({port:parseInt(process.env.MLX_VISION_PORT??"8765",10),mlxVisionModelPath:process.env.MLX_VISION_MODEL_PATH??"",mlxVisionEnabled:process.env.MLX_VISION_ENABLED!=="false",florence2ModelPath:process.env.FLORENCE2_MODEL_PATH||process.env.DETECT_MODEL_PATH||"",backend:process.env.FASTVLM_BACKEND||"mlx",maxTokens:process.env.MLX_VISION_MAX_TOKENS?parseInt(process.env.MLX_VISION_MAX_TOKENS,10):undefined,temperature:process.env.MLX_VISION_TEMPERATURE?parseFloat(process.env.MLX_VISION_TEMPERATURE):undefined,detectBackend:process.env.FASTVLM_DETECT_BACKEND||"florence2",qwen3VlModelPath:process.env.FASTVLM_QWEN3_VL_MODEL_PATH||""},msg=>{try{ctx.status(msg);}catch{}});}catch(e){throw new Error(`Failed to start detection server: ${e.message||String(e)}`)}}try{ctx.status(`Detecting objects in ${sourceEntries.length} image${sourceEntries.length===1?"":"s"}…`);}catch{}const detectPort=parseInt(process.env.MLX_VISION_PORT??"8765",10);const detectionConfig={endpoint:process.env.DETECT_ENDPOINT||DETECT_ENDPOINT||`http://localhost:${detectPort}/detect`,task,florence2ModelPath:process.env.FLORENCE2_MODEL_PATH||FLORENCE2_MODEL_PATH,timeoutMs:12e4};const tmpPaths=[];const detectionItems=[];for(const entry of sourceEntries){const tmpPath=path.join(primaryOutDir,`_tmp_detect_src_${entry.id}_${Date.now()}.png`);await fs.promises.writeFile(tmpPath,entry.buf);tmpPaths.push(tmpPath);detectionItems.push({id:entry.id,filePath:tmpPath});}console.log("[detect_object] calling detection API for",detectionItems.length,"items");let batchResult;try{batchResult=await analyzeMlxDetectionBatch(detectionItems,detectionConfig);console.log("[detect_object] detection API returned:",{results:batchResult.results.length,totalMs:batchResult.totalInferenceTimeMs});try{const totalObjects=batchResult.results.reduce((s,r)=>s+(r.objects?.length??0),0);const ms=Math.round(batchResult.totalInferenceTimeMs);ctx.status(`${totalObjects} object${totalObjects===1?"":"s"} found across ${batchResult.results.length} image${batchResult.results.length===1?"":"s"} (${ms}ms) — drawing bounding boxes…`);}catch{}}finally{for(const tp of tmpPaths)await fs.promises.unlink(tp).catch(()=>{});}if(!batchResult.results.length){return {content:[{type:"text",text:"detect_object: no results returned from Florence-2 API."}],isError:true}}const variantPreviewSpec=VARIANT_FULL_CONFIG.preview;const stamp=isoStampCompact();let nextI=Math.max(1,st.counters?.nextImageI??1);const imageRecordsForState=[];const resultEntries=[];const httpBase=await getHealthyServerBaseUrl();for(let idx=0;idx<batchResult.results.length;idx++){const detResult=batchResult.results[idx];const sourceId=sourceEntries[idx]?.id??`canvas${idx+1}`;const origBuf=sourceEntries[idx].origBuf;const currentI=nextI++;const bboxes=detResult.objects.map(o=>o.bbox);console.log(`[detect_object] drawing ${bboxes.length} bboxes for ${sourceId}...`);const annotatedBuf=await drawBboxesOnImage(origBuf,bboxes,{width:detResult.imageWidth,height:detResult.imageHeight});const baseName=`image-${stamp}-i${currentI}`;const savedPath=path.join(primaryOutDir,`${baseName}.png`);await fs.promises.writeFile(savedPath,annotatedBuf);const savedFileUrl=url.pathToFileURL(savedPath).toString();const savedSize=annotatedBuf.length;console.log(`[detect_object] annotated image written: ${savedPath} (${savedSize} bytes)`);let preview=null;try{const p=await generatePreviewFromBuffer(annotatedBuf,primaryOutDir,`${baseName}.png`,variantPreviewSpec);preview={ok:true,filePath:p.previewAbs,fileName:p.previewFilename,fileUrl:url.pathToFileURL(p.previewAbs).toString(),size_bytes:p.data.length,width:p.width,height:p.height,mimeType:"image/jpeg",dataBase64:p.data.toString("base64")};}catch(e){console.warn(`[detect_object] preview generation failed for ${sourceId}:`,String(e));}const httpOriginal=httpBase?toHttpOriginalUrl(`${baseName}.png`,httpBase,currentLmChatId||undefined):"";const httpPreview=(()=>{if(!httpBase||!currentLmChatId||!preview?.fileName)return "";return toHttpPreviewUrl(preview.fileName,httpBase,currentLmChatId)})();imageRecordsForState.push({filename:`${baseName}.png`,preview:preview?`preview-${baseName}.jpg`:undefined,i:currentI,sourceTool:`${getSelfPluginIdentifier()}/detect_object`,detectSource:sourceId,task,imageWidth:detResult.imageWidth,imageHeight:detResult.imageHeight,detections:detResult.objects.map(o=>({label:o.label,bbox:{x1:o.bbox[0],y1:o.bbox[1],x2:o.bbox[2],y2:o.bbox[3]},crop:{cropLeft:o.cropLeft,cropRight:o.cropRight,cropTop:o.cropTop,cropBottom:o.cropBottom}}))});resultEntries.push({id:sourceId,i:currentI,detResult,savedPath,savedFileUrl,savedSize,preview,httpOriginal,httpPreview});}console.log("[detect_object] updating state...");try{const stateForUpdate=await readState(primaryOutDir);const appendResult=appendImages(stateForUpdate,imageRecordsForState);if(appendResult.changed){await writeStateAtomic(primaryOutDir,stateForUpdate);console.log("[detect_object] state written, nextImageI:",stateForUpdate.counters?.nextImageI);}}catch(e){console.warn("[detect_object] state update failed:",String(e));}try{const audit=buildAuditLogger({backend:"detect_object",mode:"detect_object",requestId:undefined});if(currentLmChatId)audit.setChatId(currentLmChatId);audit.setUserRequest({targets:rawTargets,task});audit.setOutput({images:resultEntries.map(r=>({id:r.id,i:r.i,detections:r.detResult.objects.length,path:r.savedPath,url:r.savedFileUrl,bytes:r.savedSize,...r.httpOriginal?{http_url:r.httpOriginal}:{},...r.preview?{preview_path:r.preview.filePath,preview_url:r.preview.fileUrl}:{},...r.httpPreview?{http_preview_url:r.httpPreview}:{}}))});await audit.write();}catch(e){console.warn("[detect_object] audit logging failed:",String(e));}const envPreviewRaw=process.env["PREVIEW_IN_CHAT"];const previewInChat=envPreviewRaw===undefined?true:envPreviewRaw==="1"||envPreviewRaw.toLowerCase()==="true";const summaries=resultEntries.map(r=>({tool:"detect_object",source:r.id,i:r.i,imageWidth:r.detResult.imageWidth,imageHeight:r.detResult.imageHeight,task,inferenceTimeMs:r.detResult.inferenceTimeMs,detections:r.detResult.objects.map(o=>({label:o.label,bbox:{x1:o.bbox[0],y1:o.bbox[1],x2:o.bbox[2],y2:o.bbox[3]},crop:{left:{pct:o.cropLeft,px:Math.round(o.cropLeft/100*r.detResult.imageWidth)},right:{pct:o.cropRight,px:Math.round(o.cropRight/100*r.detResult.imageWidth)},top:{pct:o.cropTop,px:Math.round(o.cropTop/100*r.detResult.imageHeight)},bottom:{pct:o.cropBottom,px:Math.round(o.cropBottom/100*r.detResult.imageHeight)}},crop_tool_hint:"Pass crop.left.pct as cropLeft, crop.right.pct as cropRight, crop.top.pct as cropTop, crop.bottom.pct as cropBottom to the crop tool."}))}));const reviewHint="Carefully examine the preview and comment on how well the object detection matches your intent.";const content=[];for(const r of resultEntries){const fallbackPreviewUrl=r.preview?.fileUrl||r.savedFileUrl;const previewLine=`Preview i${r.i}: ${r.httpPreview?r.httpPreview:fallbackPreviewUrl}`;const originalLine=`Original i${r.i}: ${r.httpOriginal?r.httpOriginal:r.savedFileUrl}`;if(previewInChat&&r.preview){const fname=String(r.preview.fileName||"");content.push({type:"image",fileName:fname,mimeType:r.preview.mimeType,markdown:``,$hint:"This is an image file. Present the image to the user by using the markdown above."});}content.push({type:"text",text:previewLine});content.push({type:"text",text:originalLine});}const totalMs=Math.round(batchResult.totalInferenceTimeMs);if(totalMs>0){content.push({type:"text",text:`Total inference time: ${totalMs}ms`});}content.push({type:"text",text:JSON.stringify(summaries.length===1?summaries[0]:summaries),$hint:reviewHint});return {content}}catch(error){return {content:[{type:"text",text:`detect_object failed: ${error.message||String(error)}`}],isError:true}}}})}
async function toolsProvider(ctl){try{const getter=ctl.getGlobalPluginConfig||ctl.getGlobalConfig;const gcfg=getter?getter.call(ctl,globalConfigSchematics):null;if(gcfg){const endpoint=gcfg.get("mlxVisionEndpoint");if(typeof endpoint==="string"&&endpoint.trim()){process.env.MLX_VISION_ENDPOINT=endpoint.trim();}const detectEndpoint=gcfg.get("detectEndpoint");if(typeof detectEndpoint==="string"&&detectEndpoint.trim()){process.env.DETECT_ENDPOINT=detectEndpoint.trim();}const prompt=gcfg.get("mlxVisionPrompt");if(typeof prompt==="string"){process.env.MLX_VISION_PROMPT=prompt;}const inclMeta=gcfg.get("includeGenerationMetadata");if(typeof inclMeta==="boolean"){process.env.INCLUDE_GENERATION_METADATA=inclMeta?"true":"false";}const previewInChat=gcfg.get("PREVIEW_IN_CHAT");if(typeof previewInChat==="boolean"){process.env.PREVIEW_IN_CHAT=previewInChat?"true":"false";}const httpPort=gcfg.get("HTTP_SERVER_PORT");if(typeof httpPort==="number"&&Number.isFinite(httpPort)&&httpPort>0){process.env.HTTP_SERVER_PORT=String(Math.floor(httpPort));}const mlxModelPath=gcfg.get("mlxVisionModelPath");if(typeof mlxModelPath==="string"){process.env.MLX_VISION_MODEL_PATH=mlxModelPath;}const mlxPort=gcfg.get("mlxVisionPort");if(typeof mlxPort==="number"&&Number.isFinite(mlxPort)&&mlxPort>0){process.env.MLX_VISION_PORT=String(Math.floor(mlxPort));}const mlxMaxTokens=gcfg.get("mlxVisionMaxTokens");if(typeof mlxMaxTokens==="number"&&Number.isFinite(mlxMaxTokens)&&mlxMaxTokens>0){process.env.MLX_VISION_MAX_TOKENS=String(Math.floor(mlxMaxTokens));}const mlxTemp=gcfg.get("mlxVisionTemperature");if(typeof mlxTemp==="number"&&Number.isFinite(mlxTemp)){process.env.MLX_VISION_TEMPERATURE=String(mlxTemp);}const mlxEnabled=gcfg.get("mlxVisionEnabled");if(typeof mlxEnabled==="boolean"){process.env.MLX_VISION_ENABLED=mlxEnabled?"true":"false";}const mlxBackend=gcfg.get("mlxVisionBackend");if(typeof mlxBackend==="boolean"){process.env.FASTVLM_BACKEND=mlxBackend?"ane":"mlx";}const detectModelPath=gcfg.get("detectModelPath");if(typeof detectModelPath==="string"){process.env.DETECT_MODEL_PATH=detectModelPath;process.env.FLORENCE2_MODEL_PATH=detectModelPath;}const detectEnabled=gcfg.get("detectEnabled");if(typeof detectEnabled==="boolean"){process.env.DETECT_ENABLED=detectEnabled?"true":"false";}const detectBackend=gcfg.get("detectBackend");if(typeof detectBackend==="boolean"){process.env.FASTVLM_DETECT_BACKEND=detectBackend?"qwen3-vl":"florence2";}const qwen3VlModelPath=gcfg.get("qwen3VlModelPath");if(typeof qwen3VlModelPath==="string"){process.env.FASTVLM_QWEN3_VL_MODEL_PATH=qwen3VlModelPath;}const qwen3VlOdPrompt=gcfg.get("qwen3VlOdPrompt");if(typeof qwen3VlOdPrompt==="string"){process.env.DETECT_OD_PROMPT=qwen3VlOdPrompt;}const serverTTL=gcfg.get("serverTTL");if(typeof serverTTL==="number"&&Number.isFinite(serverTTL)&&serverTTL>=0){process.env.SERVER_TTL=String(Math.floor(serverTTL));}}}catch{}const tools=[];tools.push(createAnalyseImageTool(ctl));tools.push(createDetectObjectTool());return tools}
function shutdownHandler(signal){stopActiveFastvlmServer().finally(()=>process.exit(0));}process.on("SIGTERM",shutdownHandler);process.on("SIGINT",shutdownHandler);async function main(context){context.withGlobalConfigSchematics(globalConfigSchematics).withToolsProvider(toolsProvider);}
exports.main = main;