Forked from vadimfedenko/analyze-images
Forked from vadimfedenko/analyze-images
dist / attachmentIdentity.js
dist / attachmentIdentity.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.hashBytes = exports.IMAGE_LIMIT = void 0;
exports.fileDigest = fileDigest;
exports.candidateBytes = candidateBytes;
exports.makeCandidate = makeCandidate;
exports.readCandidate = readCandidate;
exports.recentCandidates = recentCandidates;
exports.historyDigests = historyDigests;
const node_crypto_1 = require("node:crypto");
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
exports.IMAGE_LIMIT = 8 * 1024 * 1024;
const hashBytes = (bytes) => (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex");
exports.hashBytes = hashBytes;
const cache = new Map(); // Digests only; never chat candidates or image buffers.
async function fileDigest(file, budget) {
if (!file.isImage())
return undefined;
try {
if (file.identifier.startsWith("base64:")) {
const encoded = file.identifier.slice(7);
if (encoded.length > Math.ceil(exports.IMAGE_LIMIT / 3) * 4)
return undefined;
const bytes = Buffer.from(encoded, "base64");
if (bytes.length > exports.IMAGE_LIMIT || bytes.length > budget.remaining)
return undefined;
budget.remaining -= bytes.length;
return (0, exports.hashBytes)(bytes);
}
const path = await file.getFilePath();
const info = await (0, promises_1.stat)(path);
const key = `${path}:${info.size}:${info.mtimeMs}:${info.ctimeMs}`;
const saved = cache.get(key);
if (saved)
return saved;
if (!info.isFile() || info.size > exports.IMAGE_LIMIT || info.size > budget.remaining)
return undefined;
budget.remaining -= info.size;
const bytes = await (0, promises_1.readFile)(path);
if (bytes.length > exports.IMAGE_LIMIT)
return undefined;
const digest = (0, exports.hashBytes)(bytes);
if (cache.size >= 256)
cache.delete(cache.keys().next().value);
cache.set(key, digest);
return digest;
}
catch {
return undefined;
}
}
async function candidateBytes(root, name) {
if ((0, node_path_1.isAbsolute)(name) || !name.trim())
throw new Error("Invalid relative image path.");
const directory = await (0, promises_1.realpath)(root);
const target = await (0, promises_1.realpath)((0, node_path_1.resolve)(directory, name));
const rel = (0, node_path_1.relative)(directory, target);
if (!rel || (0, node_path_1.isAbsolute)(rel) || rel === ".." || rel.startsWith("../") || rel.startsWith("..\\")) {
throw new Error("Image path escapes the working directory.");
}
const info = await (0, promises_1.stat)(target);
if (!info.isFile() || info.size > exports.IMAGE_LIMIT)
throw new Error("Candidate exceeds image size limit.");
const bytes = await (0, promises_1.readFile)(target);
if (bytes.length > exports.IMAGE_LIMIT)
throw new Error("Candidate exceeds image size limit.");
return { bytes, extension: (0, node_path_1.extname)(target).toLowerCase() };
}
async function makeCandidate(root, name) {
return (await readCandidate(root, name)).candidate;
}
async function readCandidate(root, name) {
const input = await candidateBytes(root, name);
const candidate = { version: 1, id: (0, node_crypto_1.randomUUID)(), relativePath: name, sha256: (0, exports.hashBytes)(input.bytes),
rootHash: (0, exports.hashBytes)(Buffer.from(await (0, promises_1.realpath)(root))), createdAt: Date.now() };
return { ...input, candidate };
}
// Only receipts paired with an Analyze tool call in THIS history qualify. No directory-global queue.
function recentCandidates(history) {
const messages = history.getMessagesArray().slice(-64);
const calls = new Set();
const groups = [];
for (const message of messages) {
if (message.isAssistantMessage()) {
for (const call of message.getToolCallRequests()) {
const name = call.name.replace(/[^a-z]/gi, "").toLowerCase();
if (call.id && (name.endsWith("analyzelocalimageadvanced") || name.endsWith("multianalyzelocalimagesadvanced")))
calls.add(call.id);
}
}
if (message.getRole() !== "tool")
continue;
for (const entry of message.getToolCallResults()) {
if (!entry.toolCallId || !calls.has(entry.toolCallId))
continue;
try {
const parsed = JSON.parse(entry.content);
const values = Array.isArray(parsed._aiaImageCandidates) ? parsed._aiaImageCandidates.slice(0, 4) : [parsed._aiaImageCandidate];
const group = [];
for (const value of values) {
if (value?.version !== 1 || typeof value.id !== "string" || typeof value.relativePath !== "string"
|| !/^[a-f0-9]{64}$/.test(value.sha256) || !/^[a-f0-9]{64}$/.test(value.rootHash)
|| !Number.isFinite(value.createdAt) || value.createdAt > Date.now() + 60_000
|| Date.now() - value.createdAt > 24 * 60 * 60 * 1000)
continue;
group.push(value);
}
if (group.length)
groups.push(group);
}
catch { /* Other tool outputs are not attachment candidates. */ }
}
}
return groups.reverse().flat().filter((item, i, all) => all.findIndex(other => other.sha256 === item.sha256) === i).slice(0, 8);
}
async function historyDigests(history, client, budget) {
const digests = new Set();
for (const message of history.getMessagesArray().slice(-64).reverse()) {
if (!message.isUserMessage())
continue;
for (const file of message.getFiles(client)) {
if (!file.isImage())
continue;
const stamped = /^aia-([a-f0-9]{64})\./.exec(file.name)?.[1];
const digest = stamped ?? await fileDigest(file, budget);
if (digest)
digests.add(digest);
}
}
return digests;
}
//# sourceMappingURL=attachmentIdentity.js.map"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.hashBytes = exports.IMAGE_LIMIT = void 0;
exports.fileDigest = fileDigest;
exports.candidateBytes = candidateBytes;
exports.makeCandidate = makeCandidate;
exports.readCandidate = readCandidate;
exports.recentCandidates = recentCandidates;
exports.historyDigests = historyDigests;
const node_crypto_1 = require("node:crypto");
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
exports.IMAGE_LIMIT = 8 * 1024 * 1024;
const hashBytes = (bytes) => (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex");
exports.hashBytes = hashBytes;
const cache = new Map(); // Digests only; never chat candidates or image buffers.
async function fileDigest(file, budget) {
if (!file.isImage())
return undefined;
try {
if (file.identifier.startsWith("base64:")) {
const encoded = file.identifier.slice(7);
if (encoded.length > Math.ceil(exports.IMAGE_LIMIT / 3) * 4)
return undefined;
const bytes = Buffer.from(encoded, "base64");
if (bytes.length > exports.IMAGE_LIMIT || bytes.length > budget.remaining)
return undefined;
budget.remaining -= bytes.length;
return (0, exports.hashBytes)(bytes);
}
const path = await file.getFilePath();
const info = await (0, promises_1.stat)(path);
const key = `${path}:${info.size}:${info.mtimeMs}:${info.ctimeMs}`;
const saved = cache.get(key);
if (saved)
return saved;
if (!info.isFile() || info.size > exports.IMAGE_LIMIT || info.size > budget.remaining)
return undefined;
budget.remaining -= info.size;
const bytes = await (0, promises_1.readFile)(path);
if (bytes.length > exports.IMAGE_LIMIT)
return undefined;
const digest = (0, exports.hashBytes)(bytes);
if (cache.size >= 256)
cache.delete(cache.keys().next().value);
cache.set(key, digest);
return digest;
}
catch {
return undefined;
}
}
async function candidateBytes(root, name) {
if ((0, node_path_1.isAbsolute)(name) || !name.trim())
throw new Error("Invalid relative image path.");
const directory = await (0, promises_1.realpath)(root);
const target = await (0, promises_1.realpath)((0, node_path_1.resolve)(directory, name));
const rel = (0, node_path_1.relative)(directory, target);
if (!rel || (0, node_path_1.isAbsolute)(rel) || rel === ".." || rel.startsWith("../") || rel.startsWith("..\\")) {
throw new Error("Image path escapes the working directory.");
}
const info = await (0, promises_1.stat)(target);
if (!info.isFile() || info.size > exports.IMAGE_LIMIT)
throw new Error("Candidate exceeds image size limit.");
const bytes = await (0, promises_1.readFile)(target);
if (bytes.length > exports.IMAGE_LIMIT)
throw new Error("Candidate exceeds image size limit.");
return { bytes, extension: (0, node_path_1.extname)(target).toLowerCase() };
}
async function makeCandidate(root, name) {
return (await readCandidate(root, name)).candidate;
}
async function readCandidate(root, name) {
const input = await candidateBytes(root, name);
const candidate = { version: 1, id: (0, node_crypto_1.randomUUID)(), relativePath: name, sha256: (0, exports.hashBytes)(input.bytes),
rootHash: (0, exports.hashBytes)(Buffer.from(await (0, promises_1.realpath)(root))), createdAt: Date.now() };
return { ...input, candidate };
}
// Only receipts paired with an Analyze tool call in THIS history qualify. No directory-global queue.
function recentCandidates(history) {
const messages = history.getMessagesArray().slice(-64);
const calls = new Set();
const groups = [];
for (const message of messages) {
if (message.isAssistantMessage()) {
for (const call of message.getToolCallRequests()) {
const name = call.name.replace(/[^a-z]/gi, "").toLowerCase();
if (call.id && (name.endsWith("analyzelocalimageadvanced") || name.endsWith("multianalyzelocalimagesadvanced")))
calls.add(call.id);
}
}
if (message.getRole() !== "tool")
continue;
for (const entry of message.getToolCallResults()) {
if (!entry.toolCallId || !calls.has(entry.toolCallId))
continue;
try {
const parsed = JSON.parse(entry.content);
const values = Array.isArray(parsed._aiaImageCandidates) ? parsed._aiaImageCandidates.slice(0, 4) : [parsed._aiaImageCandidate];
const group = [];
for (const value of values) {
if (value?.version !== 1 || typeof value.id !== "string" || typeof value.relativePath !== "string"
|| !/^[a-f0-9]{64}$/.test(value.sha256) || !/^[a-f0-9]{64}$/.test(value.rootHash)
|| !Number.isFinite(value.createdAt) || value.createdAt > Date.now() + 60_000
|| Date.now() - value.createdAt > 24 * 60 * 60 * 1000)
continue;
group.push(value);
}
if (group.length)
groups.push(group);
}
catch { /* Other tool outputs are not attachment candidates. */ }
}
}
return groups.reverse().flat().filter((item, i, all) => all.findIndex(other => other.sha256 === item.sha256) === i).slice(0, 8);
}
async function historyDigests(history, client, budget) {
const digests = new Set();
for (const message of history.getMessagesArray().slice(-64).reverse()) {
if (!message.isUserMessage())
continue;
for (const file of message.getFiles(client)) {
if (!file.isImage())
continue;
const stamped = /^aia-([a-f0-9]{64})\./.exec(file.name)?.[1];
const digest = stamped ?? await fileDigest(file, budget);
if (digest)
digests.add(digest);
}
}
return digests;
}
//# sourceMappingURL=attachmentIdentity.js.map