forked from WHC/Hermit
v0.7.0: image input, model registry, prefix-cache savings
Images - vision gate: text-only models get a note instead of image parts and no view_image tool; unknown models retry once without images on HTTP 400 - paste: Ctrl+V and /attach paste read the clipboard (wl-paste, xclip, pngpaste/osascript, Win32 PNG or CF_DIB converted to PNG); pasting image file paths attaches them - MCP image content is stored in the session and sent with the tool result - attach: rel path validation, base64 decode, /retry keeps images Model registry - built-in src/models_builtin.json embedded at build time, merged with <config>/models.json and cached GET /models metadata (context, vision, pricing); most specific rule wins per field - /model labels, /status source, --list-models [provider] Cache and cost - stable request prefix (frozen tools, notes as messages), DeepSeek prefix completion, OpenAI/Anthropic cache hints, /cost and hit rate in the status
This commit is contained in:
+482
@@ -0,0 +1,482 @@
|
||||
#include "cst_clipboard.h"
|
||||
#include "cst_attach.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
static void set_err(char *err, size_t err_sz, const char *msg) {
|
||||
if (err && err_sz)
|
||||
snprintf(err, err_sz, "%s", msg);
|
||||
}
|
||||
|
||||
static void put_le32(unsigned char *p, unsigned long v) {
|
||||
p[0] = (unsigned char)(v & 0xff);
|
||||
p[1] = (unsigned char)((v >> 8) & 0xff);
|
||||
p[2] = (unsigned char)((v >> 16) & 0xff);
|
||||
p[3] = (unsigned char)((v >> 24) & 0xff);
|
||||
}
|
||||
|
||||
static void put_be32(unsigned char *p, unsigned long v) {
|
||||
p[0] = (unsigned char)((v >> 24) & 0xff);
|
||||
p[1] = (unsigned char)((v >> 16) & 0xff);
|
||||
p[2] = (unsigned char)((v >> 8) & 0xff);
|
||||
p[3] = (unsigned char)(v & 0xff);
|
||||
}
|
||||
|
||||
static unsigned long get_le32(const unsigned char *p) {
|
||||
return (unsigned long)p[0] | (unsigned long)p[1] << 8 |
|
||||
(unsigned long)p[2] << 16 | (unsigned long)p[3] << 24;
|
||||
}
|
||||
|
||||
static unsigned long crc32_update(unsigned long crc, const unsigned char *p,
|
||||
size_t n) {
|
||||
static unsigned long table[256];
|
||||
static int ready;
|
||||
if (!ready) {
|
||||
for (unsigned long i = 0; i < 256; i++) {
|
||||
unsigned long c = i;
|
||||
for (int k = 0; k < 8; k++)
|
||||
c = c & 1 ? 0xedb88320UL ^ (c >> 1) : c >> 1;
|
||||
table[i] = c;
|
||||
}
|
||||
ready = 1;
|
||||
}
|
||||
crc ^= 0xffffffffUL;
|
||||
for (size_t i = 0; i < n; i++)
|
||||
crc = table[(crc ^ p[i]) & 0xff] ^ (crc >> 8);
|
||||
return crc ^ 0xffffffffUL;
|
||||
}
|
||||
|
||||
static unsigned char *png_chunk(unsigned char *o, const char *type,
|
||||
const unsigned char *data, size_t n) {
|
||||
put_be32(o, (unsigned long)n);
|
||||
memcpy(o + 4, type, 4);
|
||||
if (n)
|
||||
memcpy(o + 8, data, n);
|
||||
put_be32(o + 8 + n, crc32_update(0, o + 4, n + 4));
|
||||
return o + 12 + n;
|
||||
}
|
||||
|
||||
/* RGB rows (filter byte 0 each) in stored deflate blocks: no compressor in
|
||||
* the tree, and clipboard screenshots stay well under the attach cap. */
|
||||
static char *png_from_raw(const unsigned char *raw, size_t raw_n, long w,
|
||||
long h, size_t *n) {
|
||||
size_t blocks = raw_n / 65535 + 1;
|
||||
size_t z_n = 2 + raw_n + blocks * 5 + 4;
|
||||
size_t total = 8 + (12 + 13) + (12 + z_n) + 12;
|
||||
unsigned char *out = malloc(total);
|
||||
unsigned char *z = malloc(z_n);
|
||||
if (!out || !z) {
|
||||
free(out);
|
||||
free(z);
|
||||
return NULL;
|
||||
}
|
||||
unsigned char *q = z;
|
||||
*q++ = 0x78;
|
||||
*q++ = 0x01;
|
||||
unsigned long a = 1, b = 0;
|
||||
size_t left = raw_n, pos = 0;
|
||||
do {
|
||||
size_t len = left > 65535 ? 65535 : left;
|
||||
*q++ = left == len ? 1 : 0;
|
||||
q[0] = (unsigned char)(len & 0xff);
|
||||
q[1] = (unsigned char)(len >> 8);
|
||||
q[2] = (unsigned char)(~len & 0xff);
|
||||
q[3] = (unsigned char)((~len >> 8) & 0xff);
|
||||
q += 4;
|
||||
memcpy(q, raw + pos, len);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
a = (a + raw[pos + i]) % 65521;
|
||||
b = (b + a) % 65521;
|
||||
}
|
||||
q += len;
|
||||
pos += len;
|
||||
left -= len;
|
||||
} while (left);
|
||||
put_be32(q, b << 16 | a);
|
||||
q += 4;
|
||||
|
||||
unsigned char ihdr[13];
|
||||
put_be32(ihdr, (unsigned long)w);
|
||||
put_be32(ihdr + 4, (unsigned long)h);
|
||||
ihdr[8] = 8; /* bit depth */
|
||||
ihdr[9] = 2; /* RGB */
|
||||
ihdr[10] = ihdr[11] = ihdr[12] = 0;
|
||||
unsigned char *o = out;
|
||||
memcpy(o, "\x89PNG\r\n\x1a\n", 8);
|
||||
o = png_chunk(o + 8, "IHDR", ihdr, sizeof(ihdr));
|
||||
o = png_chunk(o, "IDAT", z, (size_t)(q - z));
|
||||
o = png_chunk(o, "IEND", NULL, 0);
|
||||
free(z);
|
||||
*n = (size_t)(o - out);
|
||||
return (char *)out;
|
||||
}
|
||||
|
||||
/* CF_DIB is a BMP without its 14-byte file header; the pixel offset depends
|
||||
* on the header size, BI_BITFIELDS masks and the color table. */
|
||||
char *cst_dib_to_image(const void *dib_v, size_t sz, size_t *n) {
|
||||
const unsigned char *dib = dib_v;
|
||||
if (!dib || sz < 40)
|
||||
return NULL;
|
||||
unsigned long hsize = get_le32(dib);
|
||||
long w = (long)(int)get_le32(dib + 4);
|
||||
long hh = (long)(int)get_le32(dib + 8);
|
||||
unsigned bits = (unsigned)(dib[14] | dib[15] << 8);
|
||||
unsigned long comp = get_le32(dib + 16);
|
||||
unsigned long used = get_le32(dib + 32);
|
||||
if (hsize < 40 || hsize > sz)
|
||||
return NULL;
|
||||
size_t off = hsize;
|
||||
if (hsize == 40 && comp == 3 /* BI_BITFIELDS */)
|
||||
off += 12;
|
||||
size_t colors = used;
|
||||
if (!colors && bits <= 8)
|
||||
colors = (size_t)1 << bits;
|
||||
if (colors > 256)
|
||||
return NULL;
|
||||
off += colors * 4;
|
||||
if (off > sz)
|
||||
return NULL;
|
||||
|
||||
long h = hh < 0 ? -hh : hh;
|
||||
if ((bits == 24 || bits == 32) && (comp == 0 || comp == 3) && w > 0 &&
|
||||
h > 0 && w <= 32768 && h <= 32768) {
|
||||
size_t stride = ((size_t)w * bits / 8 + 3) & ~(size_t)3;
|
||||
size_t raw_n = (size_t)h * (1 + (size_t)w * 3);
|
||||
if (stride * (size_t)h <= sz - off && raw_n < CST_ATTACH_MAX_BYTES) {
|
||||
unsigned char *raw = malloc(raw_n);
|
||||
if (!raw)
|
||||
return NULL;
|
||||
size_t bpp = bits / 8;
|
||||
for (long y = 0; y < h; y++) {
|
||||
long src_y = hh > 0 ? h - 1 - y : y; /* positive height = bottom-up */
|
||||
const unsigned char *s = dib + off + (size_t)src_y * stride;
|
||||
unsigned char *d = raw + (size_t)y * (1 + (size_t)w * 3);
|
||||
*d++ = 0;
|
||||
for (long x = 0; x < w; x++, s += bpp) {
|
||||
*d++ = s[2];
|
||||
*d++ = s[1];
|
||||
*d++ = s[0];
|
||||
}
|
||||
}
|
||||
char *png = png_from_raw(raw, raw_n, w, h, n);
|
||||
free(raw);
|
||||
return png;
|
||||
}
|
||||
}
|
||||
|
||||
char *out = malloc(14 + sz);
|
||||
if (!out)
|
||||
return NULL;
|
||||
unsigned char *hd = (unsigned char *)out;
|
||||
hd[0] = 'B';
|
||||
hd[1] = 'M';
|
||||
put_le32(hd + 2, (unsigned long)(14 + sz));
|
||||
put_le32(hd + 6, 0);
|
||||
put_le32(hd + 10, (unsigned long)(14 + off));
|
||||
memcpy(out + 14, dib, sz);
|
||||
*n = 14 + sz;
|
||||
return out;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
int cst_clipboard_image(char **data, size_t *n, char *err, size_t err_sz) {
|
||||
*data = NULL;
|
||||
*n = 0;
|
||||
UINT png = RegisterClipboardFormatA("PNG");
|
||||
if (!OpenClipboard(NULL)) {
|
||||
set_err(err, err_sz, "could not open the clipboard");
|
||||
return -1;
|
||||
}
|
||||
HANDLE h = NULL;
|
||||
int is_png = 0;
|
||||
if (png && IsClipboardFormatAvailable(png)) {
|
||||
h = GetClipboardData(png);
|
||||
is_png = 1;
|
||||
} else if (IsClipboardFormatAvailable(CF_DIB)) {
|
||||
h = GetClipboardData(CF_DIB);
|
||||
}
|
||||
const unsigned char *p = h ? GlobalLock(h) : NULL;
|
||||
if (!p) {
|
||||
CloseClipboard();
|
||||
set_err(err, err_sz, "no image on the clipboard");
|
||||
return -1;
|
||||
}
|
||||
size_t sz = (size_t)GlobalSize(h);
|
||||
char *out = NULL;
|
||||
if (sz > CST_ATTACH_MAX_BYTES) {
|
||||
set_err(err, err_sz, "clipboard image is too large");
|
||||
} else if (is_png) {
|
||||
out = malloc(sz ? sz : 1);
|
||||
if (out) {
|
||||
memcpy(out, p, sz);
|
||||
*n = sz;
|
||||
}
|
||||
} else {
|
||||
out = cst_dib_to_image(p, sz, n);
|
||||
if (!out)
|
||||
set_err(err, err_sz, "clipboard bitmap is malformed");
|
||||
}
|
||||
GlobalUnlock(h);
|
||||
CloseClipboard();
|
||||
if (!out)
|
||||
return -1;
|
||||
*data = out;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
/* Runs cmd (stderr already redirected by the caller); 127 = not installed. */
|
||||
static int run_bytes(const char *cmd, char **data, size_t *n) {
|
||||
os_run_result_t r;
|
||||
memset(&r, 0, sizeof(r));
|
||||
if (os_run_shell_c(NULL, cmd, CST_ATTACH_MAX_BYTES + 1, &r) != 0) {
|
||||
os_run_result_free(&r);
|
||||
return -1;
|
||||
}
|
||||
if (r.exit_code != 0 || r.out_len == 0) {
|
||||
int code = r.exit_code == 127 ? 127 : -1;
|
||||
os_run_result_free(&r);
|
||||
return code;
|
||||
}
|
||||
*data = r.out;
|
||||
*n = r.out_len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int type_ok(const char *s, size_t n) {
|
||||
if (n < 7 || n > 63 || strncmp(s, "image/", 6) != 0)
|
||||
return 0;
|
||||
for (size_t i = 0; i < n; i++)
|
||||
if (!isalnum((unsigned char)s[i]) && !strchr("/.+-", s[i]))
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Best image MIME type from a newline-separated list: PNG first. */
|
||||
static int pick_type(const char *list, size_t len, char *out, size_t out_sz) {
|
||||
out[0] = 0;
|
||||
size_t i = 0;
|
||||
while (i < len) {
|
||||
size_t j = i;
|
||||
while (j < len && list[j] != '\n' && list[j] != '\r')
|
||||
j++;
|
||||
if (type_ok(list + i, j - i)) {
|
||||
int is_png = j - i == 9 && strncmp(list + i, "image/png", 9) == 0;
|
||||
if (is_png || !out[0])
|
||||
snprintf(out, out_sz, "%.*s", (int)(j - i), list + i);
|
||||
if (is_png)
|
||||
break;
|
||||
}
|
||||
i = j + 1;
|
||||
}
|
||||
return out[0] ? 0 : -1;
|
||||
}
|
||||
|
||||
static int via_lister(const char *list_cmd, const char *get_fmt,
|
||||
const char *tool, char **data, size_t *n, char *err,
|
||||
size_t err_sz) {
|
||||
char *types = NULL;
|
||||
size_t tn = 0;
|
||||
int rc = run_bytes(list_cmd, &types, &tn);
|
||||
if (rc == 127) {
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof(msg), "reading the clipboard needs %s", tool);
|
||||
set_err(err, err_sz, msg);
|
||||
return -1;
|
||||
}
|
||||
char type[64];
|
||||
int ok = rc == 0 && pick_type(types, tn, type, sizeof(type)) == 0;
|
||||
free(types);
|
||||
if (!ok) {
|
||||
set_err(err, err_sz, "no image on the clipboard");
|
||||
return -1;
|
||||
}
|
||||
char cmd[256];
|
||||
snprintf(cmd, sizeof(cmd), get_fmt, type);
|
||||
if (run_bytes(cmd, data, n) != 0) {
|
||||
set_err(err, err_sz, "could not read the clipboard image");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
static int mac_image(char **data, size_t *n, char *err, size_t err_sz) {
|
||||
int rc = run_bytes("pngpaste - 2>/dev/null", data, n);
|
||||
if (rc == 0)
|
||||
return 0;
|
||||
char path[1024];
|
||||
int fd = os_mkstemp(path, sizeof(path), "cst-clip");
|
||||
if (fd < 0) {
|
||||
set_err(err, err_sz, "could not create a temp file");
|
||||
return -1;
|
||||
}
|
||||
close(fd);
|
||||
char cmd[1600];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"osascript -e 'set f to open for access (POSIX file \"%s\") with "
|
||||
"write permission' -e 'set eof f to 0' -e 'write (the clipboard as "
|
||||
"\xc2\xabclass PNGf\xc2\xbb) to f' -e 'close access f' "
|
||||
">/dev/null 2>&1",
|
||||
path);
|
||||
os_run_result_t r;
|
||||
memset(&r, 0, sizeof(r));
|
||||
int ran = os_run_shell_c(NULL, cmd, 4096, &r);
|
||||
int code = r.exit_code;
|
||||
os_run_result_free(&r);
|
||||
char *buf = NULL;
|
||||
size_t len = 0;
|
||||
if (ran == 0 && code == 0) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (f) {
|
||||
char chunk[65536];
|
||||
size_t got;
|
||||
while ((got = fread(chunk, 1, sizeof(chunk), f)) > 0 &&
|
||||
len <= CST_ATTACH_MAX_BYTES) {
|
||||
char *nb = realloc(buf, len + got);
|
||||
if (!nb)
|
||||
break;
|
||||
buf = nb;
|
||||
memcpy(buf + len, chunk, got);
|
||||
len += got;
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
remove(path);
|
||||
if (!buf || !len) {
|
||||
free(buf);
|
||||
set_err(err, err_sz, "no image on the clipboard");
|
||||
return -1;
|
||||
}
|
||||
*data = buf;
|
||||
*n = len;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
int cst_clipboard_image(char **data, size_t *n, char *err, size_t err_sz) {
|
||||
*data = NULL;
|
||||
*n = 0;
|
||||
#ifdef __APPLE__
|
||||
return mac_image(data, n, err, err_sz);
|
||||
#else
|
||||
const char *wl = getenv("WAYLAND_DISPLAY");
|
||||
if (wl && *wl)
|
||||
return via_lister("wl-paste --list-types 2>/dev/null",
|
||||
"wl-paste --no-newline --type '%s' 2>/dev/null",
|
||||
"wl-clipboard (wl-paste)", data, n, err, err_sz);
|
||||
const char *x = getenv("DISPLAY");
|
||||
if (x && *x)
|
||||
return via_lister("xclip -selection clipboard -t TARGETS -o 2>/dev/null",
|
||||
"xclip -selection clipboard -t '%s' -o 2>/dev/null",
|
||||
"xclip", data, n, err, err_sz);
|
||||
set_err(err, err_sz, "no clipboard here (no Wayland or X11 display)");
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
static int hexval(int c) {
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
if (c >= 'a' && c <= 'f')
|
||||
return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F')
|
||||
return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* file://[host]/path -> /path, percent-decoded; 0 on success. */
|
||||
static int from_file_uri(char *s) {
|
||||
const char *p = s + 7;
|
||||
const char *slash = strchr(p, '/');
|
||||
if (!slash)
|
||||
return -1;
|
||||
if (slash != p && !(slash - p == 9 && strncmp(p, "localhost", 9) == 0))
|
||||
return -1;
|
||||
char *o = s;
|
||||
for (const char *q = slash; *q; q++) {
|
||||
if (*q == '%' && hexval(q[1]) >= 0 && hexval(q[2]) >= 0) {
|
||||
*o++ = (char)(hexval(q[1]) * 16 + hexval(q[2]));
|
||||
q += 2;
|
||||
} else {
|
||||
*o++ = *q;
|
||||
}
|
||||
}
|
||||
*o = 0;
|
||||
#ifdef _WIN32
|
||||
if (s[0] == '/' && isalpha((unsigned char)s[1]) && s[2] == ':')
|
||||
memmove(s, s + 1, strlen(s));
|
||||
#endif
|
||||
return s[0] ? 0 : -1;
|
||||
}
|
||||
|
||||
static int is_space(char c) {
|
||||
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
|
||||
}
|
||||
|
||||
int cst_paste_paths(const char *text, size_t len,
|
||||
char out[][CST_PASTE_PATH_MAX], int max) {
|
||||
int n = 0;
|
||||
size_t i = 0;
|
||||
while (i < len) {
|
||||
while (i < len && is_space(text[i]))
|
||||
i++;
|
||||
if (i >= len)
|
||||
break;
|
||||
if (n >= max)
|
||||
return -1;
|
||||
char *tok = out[n];
|
||||
size_t k = 0;
|
||||
char q = 0;
|
||||
while (i < len) {
|
||||
char c = text[i];
|
||||
if (!c)
|
||||
return -1;
|
||||
if (q) {
|
||||
if (c == q) {
|
||||
q = 0;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (c == '\'' || c == '"') {
|
||||
q = c;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (is_space(c))
|
||||
break;
|
||||
#ifndef _WIN32
|
||||
if (c == '\\' && i + 1 < len)
|
||||
c = text[++i];
|
||||
#endif
|
||||
}
|
||||
if (k + 1 >= CST_PASTE_PATH_MAX)
|
||||
return -1;
|
||||
tok[k++] = c;
|
||||
i++;
|
||||
}
|
||||
if (q || !k)
|
||||
return -1;
|
||||
tok[k] = 0;
|
||||
if (strncmp(tok, "file://", 7) == 0 && from_file_uri(tok) != 0)
|
||||
return -1;
|
||||
n++;
|
||||
}
|
||||
return n > 0 ? n : -1;
|
||||
}
|
||||
Reference in New Issue
Block a user