dsh-ntr 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/impl.js CHANGED
@@ -834,6 +834,42 @@ export function childEnv(parent, extra) {
834
834
  return out;
835
835
  }
836
836
  // ---- Model catalog (pure parsing of ACP `configOptions`) ----
837
+ /**
838
+ * ACP categories are free-form UX hints, not identity. Qoder CN tags
839
+ * `reasoning_effort` as `category: 'model'` (the spec means `thought_level`),
840
+ * and nothing stops an agent from reusing a category for several options.
841
+ * Slots are therefore assigned by `configId` first and category second, and a
842
+ * same-category option NEVER replaces another option's slot: whatever loses
843
+ * stays reachable through `extras` instead of silently becoming the model list.
844
+ */
845
+ const EFFORT_OPTION_HINT = /effort|thinking|thought|reason/i;
846
+ function isModelSelector(configId, name) {
847
+ return configId === 'model' || name.trim().toLowerCase() === 'model';
848
+ }
849
+ function catalogSlotFor(category, configId, name) {
850
+ const effortish = EFFORT_OPTION_HINT.test(configId) || EFFORT_OPTION_HINT.test(name);
851
+ if (category === 'model') {
852
+ // The option explicitly named `model` is the selector even when other
853
+ // model-scoped options are present.
854
+ if (isModelSelector(configId, name))
855
+ return { slot: 'model', rank: 3 };
856
+ // A model-scoped effort is mislabelled, not a model: route it to the
857
+ // thought chip when no real `thought_level` option is published.
858
+ if (effortish)
859
+ return { slot: 'thought', rank: 1 };
860
+ // An unknown model-scoped select: a last-resort selector only.
861
+ return { slot: 'model', rank: 1 };
862
+ }
863
+ if (category === 'thought_level')
864
+ return { slot: 'thought', rank: 3 };
865
+ if (category === 'mode')
866
+ return { slot: 'mode', rank: 2 };
867
+ // `model_config` is where the spec puts model-dependent knobs (effort,
868
+ // context size); only the effort-shaped ones have a dedicated control.
869
+ if (category === 'model_config' && effortish)
870
+ return { slot: 'thought', rank: 1 };
871
+ return undefined;
872
+ }
837
873
  /**
838
874
  * Extract select options from a raw ACP `configOptions` payload.
839
875
  *
@@ -848,19 +884,13 @@ export function parseCatalogOptions(configOptions) {
848
884
  const out = {};
849
885
  if (!Array.isArray(configOptions))
850
886
  return out;
851
- const extras = new Map();
887
+ const definitions = [];
888
+ const candidates = [];
852
889
  for (const raw of configOptions) {
853
890
  if (typeof raw !== 'object' || raw === null)
854
891
  continue;
855
892
  const row = raw;
856
893
  const category = typeof row.category === 'string' ? row.category : undefined;
857
- const slot = category === 'model'
858
- ? 'model'
859
- : category === 'thought_level'
860
- ? 'thought'
861
- : category === 'mode'
862
- ? 'mode'
863
- : undefined;
864
894
  const configIdRaw = row.configId ?? row.id;
865
895
  if (typeof configIdRaw !== 'string' && typeof configIdRaw !== 'number')
866
896
  continue;
@@ -869,6 +899,7 @@ export function parseCatalogOptions(configOptions) {
869
899
  continue;
870
900
  const choices = [];
871
901
  collectCatalogChoices(row.options, choices);
902
+ const name = typeof row.name === 'string' && row.name !== '' ? row.name : configId;
872
903
  // A boolean toggle has no `options` at all; its current value is a JSON
873
904
  // boolean. It is encoded as 'true' / 'false' so the catalog stays
874
905
  // string-typed for every existing reader, and carries `kind` so the
@@ -877,7 +908,7 @@ export function parseCatalogOptions(configOptions) {
877
908
  || (typeof row.currentValue === 'boolean' && choices.length === 0);
878
909
  const option = {
879
910
  configId,
880
- name: typeof row.name === 'string' && row.name !== '' ? row.name : configId,
911
+ name,
881
912
  ...(booleanKind ? { kind: 'boolean' } : {}),
882
913
  currentValue: booleanKind
883
914
  ? row.currentValue === true || row.currentValue === 'true'
@@ -886,22 +917,81 @@ export function parseCatalogOptions(configOptions) {
886
917
  : typeof row.currentValue === 'string' && row.currentValue !== '' ? row.currentValue : null,
887
918
  choices,
888
919
  };
920
+ definitions.push(option);
889
921
  // A toggle is never a model / thought / mode SELECT: those controls pick
890
922
  // among choices. It belongs to the advanced base, where it is a checkbox.
891
- if (slot !== undefined && !booleanKind) {
892
- out[slot] = option;
923
+ if (booleanKind)
893
924
  continue;
925
+ const slot = catalogSlotFor(category, configId, name);
926
+ if (slot !== undefined)
927
+ candidates.push({ ...slot, option });
928
+ }
929
+ const winnerIds = new Set();
930
+ for (const slot of ['model', 'thought', 'mode']) {
931
+ let winner;
932
+ for (const candidate of candidates) {
933
+ if (candidate.slot !== slot)
934
+ continue;
935
+ if (winner === undefined || candidate.rank > winner.rank) {
936
+ winner = candidate;
937
+ }
938
+ else if (candidate.rank === winner.rank && candidate.option.configId === winner.option.configId) {
939
+ // The same option redefined later in one answer: the last definition
940
+ // is the current one.
941
+ winner = candidate;
942
+ }
894
943
  }
895
- // Choice-less options have nothing to pick; they would render an empty
896
- // section in the advanced menu, so they stay out of the catalog entirely —
897
- // except a boolean, whose whole point is that it has no choices.
898
- if (choices.length > 0 || booleanKind)
899
- extras.set(configId, option);
944
+ if (winner === undefined)
945
+ continue;
946
+ out[slot] = winner.option;
947
+ winnerIds.add(winner.option.configId);
948
+ }
949
+ // Choice-less options have nothing to pick; they would render an empty
950
+ // section in the advanced menu, so they stay out of the catalog entirely —
951
+ // except a boolean, whose whole point is that it has no choices. Anything
952
+ // that lost a slot race is still a real, settable option: keep it here.
953
+ const extras = new Map();
954
+ for (const option of definitions) {
955
+ if (winnerIds.has(option.configId))
956
+ continue;
957
+ if (option.choices.length > 0 || option.kind === 'boolean')
958
+ extras.set(option.configId, option);
900
959
  }
901
960
  if (extras.size > 0)
902
961
  out.extras = [...extras.values()];
903
962
  return out;
904
963
  }
964
+ /**
965
+ * Whether a payload names ONE model selector without relying on category
966
+ * alone. When an agent publishes several `category: 'model'` options and none
967
+ * is explicitly the `model` id, the category is not trustworthy enough to
968
+ * reject the user's pick — callers must fail open instead of reporting
969
+ * `model_unavailable`.
970
+ */
971
+ export function hasUnambiguousModelSelector(configOptions) {
972
+ if (!Array.isArray(configOptions))
973
+ return true;
974
+ let scoped = 0;
975
+ let explicit = false;
976
+ for (const raw of configOptions) {
977
+ if (typeof raw !== 'object' || raw === null)
978
+ continue;
979
+ const row = raw;
980
+ if (row.category !== 'model')
981
+ continue;
982
+ const configIdRaw = row.configId ?? row.id;
983
+ if (typeof configIdRaw !== 'string' && typeof configIdRaw !== 'number')
984
+ continue;
985
+ const configId = String(configIdRaw);
986
+ if (configId === '')
987
+ continue;
988
+ scoped += 1;
989
+ const name = typeof row.name === 'string' ? row.name : '';
990
+ if (isModelSelector(configId, name))
991
+ explicit = true;
992
+ }
993
+ return explicit || scoped <= 1;
994
+ }
905
995
  /**
906
996
  * Fold one per-session option answer's SHAPE into an agent-level catalog.
907
997
  *
@@ -1310,6 +1400,1068 @@ export function describeUnhandledUpdate(kind, update) {
1310
1400
  }
1311
1401
  return `${label}\n${dumpJson(update)}`;
1312
1402
  }
1403
+ /** Helper: guess MIME type from file extension or URL */
1404
+ export function guessImageMimeType(pathOrUrl) {
1405
+ if (!pathOrUrl)
1406
+ return undefined;
1407
+ const clean = pathOrUrl.split('?')[0].split('#')[0].toLowerCase();
1408
+ if (clean.endsWith('.jpg') || clean.endsWith('.jpeg'))
1409
+ return 'image/jpeg';
1410
+ if (clean.endsWith('.png'))
1411
+ return 'image/png';
1412
+ if (clean.endsWith('.gif'))
1413
+ return 'image/gif';
1414
+ if (clean.endsWith('.webp'))
1415
+ return 'image/webp';
1416
+ if (clean.endsWith('.svg') || clean.endsWith('.svgz'))
1417
+ return 'image/svg+xml';
1418
+ if (clean.endsWith('.bmp'))
1419
+ return 'image/bmp';
1420
+ if (clean.endsWith('.ico'))
1421
+ return 'image/x-icon';
1422
+ if (clean.endsWith('.tiff') || clean.endsWith('.tif'))
1423
+ return 'image/tiff';
1424
+ if (clean.endsWith('.avif'))
1425
+ return 'image/avif';
1426
+ if (clean.endsWith('.heic'))
1427
+ return 'image/heic';
1428
+ if (clean.endsWith('.heif'))
1429
+ return 'image/heif';
1430
+ if (clean.endsWith('.apng'))
1431
+ return 'image/apng';
1432
+ return undefined;
1433
+ }
1434
+ /** Helper: parse Data URI */
1435
+ export function parseImageDataUrl(url) {
1436
+ if (!url.startsWith('data:'))
1437
+ return undefined;
1438
+ const commaIdx = url.indexOf(',');
1439
+ if (commaIdx === -1)
1440
+ return undefined;
1441
+ const header = url.slice(5, commaIdx);
1442
+ const raw = url.slice(commaIdx + 1).trim();
1443
+ const parts = header.split(';');
1444
+ const mimeType = parts[0] && parts[0].trim() !== '' ? parts[0].trim().toLowerCase() : 'image/png';
1445
+ if (!mimeType.startsWith('image/'))
1446
+ return undefined;
1447
+ const isBase64 = parts.some(p => p.trim() === 'base64');
1448
+ let data;
1449
+ if (isBase64) {
1450
+ data = raw.replace(/\s+/g, '');
1451
+ }
1452
+ else {
1453
+ try {
1454
+ const decoded = decodeURIComponent(raw);
1455
+ data = typeof Buffer !== 'undefined' ? Buffer.from(decoded).toString('base64') : btoa(decoded);
1456
+ }
1457
+ catch {
1458
+ data = typeof Buffer !== 'undefined' ? Buffer.from(raw).toString('base64') : btoa(raw);
1459
+ }
1460
+ }
1461
+ return { mimeType, data };
1462
+ }
1463
+ export function isLocalFilePath(str) {
1464
+ if (typeof str !== 'string' || str === '')
1465
+ return false;
1466
+ return isAbsolutePath(str) || str.startsWith('./') || str.startsWith('.\\') || str.startsWith('../') || str.startsWith('..\\');
1467
+ }
1468
+ export function normalizeFileUrlToPath(uri) {
1469
+ if (!uri.startsWith('file://'))
1470
+ return uri;
1471
+ let raw = uri.slice('file://'.length);
1472
+ try {
1473
+ raw = decodeURIComponent(raw);
1474
+ }
1475
+ catch { }
1476
+ if (raw.startsWith('localhost/')) {
1477
+ raw = raw.slice('localhost'.length);
1478
+ }
1479
+ if (/^\/?[a-zA-Z]:/.test(raw)) {
1480
+ return raw.startsWith('/') ? raw.slice(1) : raw;
1481
+ }
1482
+ if (raw.startsWith('/')) {
1483
+ return raw;
1484
+ }
1485
+ return `//${raw}`;
1486
+ }
1487
+ function expandHomePath(p) {
1488
+ if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) {
1489
+ if (typeof process !== 'undefined' && process.env) {
1490
+ const env = process.env;
1491
+ const home = env.HOME || env.USERPROFILE || (env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : undefined);
1492
+ if (home) {
1493
+ const cleanHome = home.replaceAll('\\', '/').replace(/\/+$/, '');
1494
+ return p === '~' ? cleanHome : `${cleanHome}/${p.slice(2).replaceAll('\\', '/')}`;
1495
+ }
1496
+ }
1497
+ }
1498
+ return p;
1499
+ }
1500
+ function readImageFileFromContext(filePath, context) {
1501
+ const target = normalizeFileUrlToPath(filePath);
1502
+ const expanded = expandHomePath(target);
1503
+ const pathsToTry = target === expanded ? [target] : [target, expanded];
1504
+ const builtinFs = typeof process !== 'undefined' && typeof process.getBuiltinModule === 'function'
1505
+ ? process.getBuiltinModule('fs')
1506
+ : null;
1507
+ for (const p of pathsToTry) {
1508
+ if (context && typeof context.readFile === 'function') {
1509
+ try {
1510
+ const res = context.readFile(p);
1511
+ if (res !== undefined)
1512
+ return res;
1513
+ }
1514
+ catch { }
1515
+ }
1516
+ if (context && typeof context.fs?.readFileSync === 'function') {
1517
+ try {
1518
+ const res = context.fs.readFileSync(p);
1519
+ if (res !== undefined)
1520
+ return res;
1521
+ }
1522
+ catch { }
1523
+ }
1524
+ if (builtinFs && typeof builtinFs.existsSync === 'function' && typeof builtinFs.readFileSync === 'function') {
1525
+ try {
1526
+ if (builtinFs.existsSync(p))
1527
+ return builtinFs.readFileSync(p);
1528
+ }
1529
+ catch { }
1530
+ }
1531
+ }
1532
+ return undefined;
1533
+ }
1534
+ /**
1535
+ * Detect image MIME type by examining binary magic numbers.
1536
+ * Supports Buffer, Uint8Array, and base64 strings.
1537
+ */
1538
+ export function detectImageMimeFromMagic(raw) {
1539
+ if (!raw)
1540
+ return undefined;
1541
+ let bytes;
1542
+ if (typeof raw === 'string') {
1543
+ const trimmed = raw.trim();
1544
+ if (trimmed.startsWith('<svg') || (trimmed.startsWith('<?xml') && trimmed.includes('<svg'))) {
1545
+ return 'image/svg+xml';
1546
+ }
1547
+ try {
1548
+ const prefix = trimmed.slice(0, 64).replace(/\s+/g, '');
1549
+ if (typeof Buffer !== 'undefined') {
1550
+ bytes = Buffer.from(prefix, 'base64');
1551
+ }
1552
+ else if (typeof atob !== 'undefined') {
1553
+ const bin = atob(prefix);
1554
+ bytes = new Uint8Array(bin.length);
1555
+ for (let i = 0; i < bin.length; i++)
1556
+ bytes[i] = bin.charCodeAt(i);
1557
+ }
1558
+ }
1559
+ catch {
1560
+ return undefined;
1561
+ }
1562
+ }
1563
+ else if (raw instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(raw))) {
1564
+ bytes = raw instanceof Uint8Array ? raw : new Uint8Array(raw);
1565
+ }
1566
+ if (!bytes || bytes.length < 3)
1567
+ return undefined;
1568
+ // PNG: 89 50 4E 47 0D 0A 1A 0A
1569
+ if (bytes.length >= 8 &&
1570
+ bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 &&
1571
+ bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a) {
1572
+ return 'image/png';
1573
+ }
1574
+ // JPEG: FF D8 FF
1575
+ if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
1576
+ return 'image/jpeg';
1577
+ }
1578
+ // GIF: GIF87a or GIF89a (47 49 46 38)
1579
+ if (bytes.length >= 6 &&
1580
+ bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38 &&
1581
+ (bytes[4] === 0x37 || bytes[4] === 0x39) && bytes[5] === 0x61) {
1582
+ return 'image/gif';
1583
+ }
1584
+ // WebP: RIFF....WEBP (52 49 46 46 .... 57 45 42 50)
1585
+ if (bytes.length >= 12 &&
1586
+ bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&
1587
+ bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) {
1588
+ return 'image/webp';
1589
+ }
1590
+ // BMP: 42 4D ("BM")
1591
+ if (bytes.length >= 2 && bytes[0] === 0x42 && bytes[1] === 0x4d) {
1592
+ return 'image/bmp';
1593
+ }
1594
+ // ICO: 00 00 01 00
1595
+ if (bytes.length >= 4 && bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 && bytes[3] === 0x00) {
1596
+ return 'image/x-icon';
1597
+ }
1598
+ // TIFF: II*. (49 49 2A 00) or MM.* (4D 4D 00 2A)
1599
+ if (bytes.length >= 4 &&
1600
+ ((bytes[0] === 0x49 && bytes[1] === 0x49 && bytes[2] === 0x2a && bytes[3] === 0x00) ||
1601
+ (bytes[0] === 0x4d && bytes[1] === 0x4d && bytes[2] === 0x00 && bytes[3] === 0x2a))) {
1602
+ return 'image/tiff';
1603
+ }
1604
+ // AVIF / HEIC: ftyp box at offset 4 (bytes 4..7 === 'ftyp')
1605
+ if (bytes.length >= 12 &&
1606
+ bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) {
1607
+ const brand = String.fromCharCode(bytes[8], bytes[9], bytes[10], bytes[11]).toLowerCase();
1608
+ if (brand === 'avif' || brand === 'avis')
1609
+ return 'image/avif';
1610
+ if (brand === 'heic' || brand === 'heix' || brand === 'hevc' || brand === 'mif1' || brand === 'msf1')
1611
+ return 'image/heic';
1612
+ }
1613
+ return undefined;
1614
+ }
1615
+ function tryResolveFromAttachmentStore(store, id, context, attachmentMeta) {
1616
+ const idWithoutPrefix = id.startsWith('sha256:') ? id.slice('sha256:'.length) : undefined;
1617
+ const idWithPrefix = id.startsWith('sha256:') ? id : `sha256:${id}`;
1618
+ const candidates = [];
1619
+ if (attachmentMeta && typeof attachmentMeta === 'object') {
1620
+ candidates.push(attachmentMeta);
1621
+ }
1622
+ candidates.push({ attachmentId: idWithPrefix });
1623
+ if (idWithoutPrefix !== undefined) {
1624
+ candidates.push({ attachmentId: idWithoutPrefix });
1625
+ }
1626
+ candidates.push({ attachmentId: id });
1627
+ candidates.push({ id: idWithPrefix });
1628
+ candidates.push({ id });
1629
+ if (idWithoutPrefix !== undefined) {
1630
+ candidates.push({ id: idWithoutPrefix });
1631
+ }
1632
+ candidates.push(id);
1633
+ if (idWithoutPrefix !== undefined) {
1634
+ candidates.push(idWithoutPrefix);
1635
+ }
1636
+ // Cordis proxies wrap the underlying service instance; if a method call on the proxy fails
1637
+ // or loses this context, fall back to the unwrapped instance (never the class prototype,
1638
+ // which lacks instance state such as `this.root`).
1639
+ const cordisOriginal = Reflect.get(store, Symbol.for('cordis.original'));
1640
+ // A. LocalAttachmentStore.imageHostPath(ref) -> synchronous local absolute path
1641
+ if (typeof store.imageHostPath === 'function') {
1642
+ const fn = store.imageHostPath;
1643
+ for (const cand of candidates) {
1644
+ if (cand === undefined)
1645
+ continue;
1646
+ try {
1647
+ let hostPath;
1648
+ try {
1649
+ hostPath = fn.call(store, cand);
1650
+ }
1651
+ catch {
1652
+ if (cordisOriginal && typeof cordisOriginal.imageHostPath === 'function') {
1653
+ hostPath = cordisOriginal.imageHostPath.call(cordisOriginal, cand);
1654
+ }
1655
+ }
1656
+ if (hostPath && typeof hostPath.then === 'function') {
1657
+ if (typeof hostPath.catch === 'function') {
1658
+ hostPath.catch(() => { });
1659
+ }
1660
+ }
1661
+ else if (typeof hostPath === 'string' && hostPath.trim() !== '') {
1662
+ const fileContent = readImageFileFromContext(hostPath, context);
1663
+ return {
1664
+ path: hostPath,
1665
+ ...(fileContent !== undefined ? { data: fileContent } : {}),
1666
+ };
1667
+ }
1668
+ }
1669
+ catch { }
1670
+ }
1671
+ }
1672
+ // B. LocalAttachmentStore.fileHostPath(ref) -> fallback for file-type attachments
1673
+ if (typeof store.fileHostPath === 'function') {
1674
+ const fn = store.fileHostPath;
1675
+ for (const cand of candidates) {
1676
+ if (cand === undefined)
1677
+ continue;
1678
+ try {
1679
+ let hostPath;
1680
+ try {
1681
+ hostPath = fn.call(store, cand);
1682
+ }
1683
+ catch {
1684
+ if (cordisOriginal && typeof cordisOriginal.fileHostPath === 'function') {
1685
+ hostPath = cordisOriginal.fileHostPath.call(cordisOriginal, cand);
1686
+ }
1687
+ }
1688
+ if (hostPath && typeof hostPath.then === 'function') {
1689
+ if (typeof hostPath.catch === 'function') {
1690
+ hostPath.catch(() => { });
1691
+ }
1692
+ }
1693
+ else if (typeof hostPath === 'string' && hostPath.trim() !== '') {
1694
+ const fileContent = readImageFileFromContext(hostPath, context);
1695
+ return {
1696
+ path: hostPath,
1697
+ ...(fileContent !== undefined ? { data: fileContent } : {}),
1698
+ };
1699
+ }
1700
+ }
1701
+ catch { }
1702
+ }
1703
+ }
1704
+ // C. LocalAttachmentStore.readImage(ref) -> { ref, data: Uint8Array }
1705
+ if (typeof store.readImage === 'function') {
1706
+ const fn = store.readImage;
1707
+ for (const cand of candidates) {
1708
+ if (cand === undefined)
1709
+ continue;
1710
+ try {
1711
+ let res;
1712
+ try {
1713
+ res = fn.call(store, cand);
1714
+ }
1715
+ catch {
1716
+ if (cordisOriginal && typeof cordisOriginal.readImage === 'function') {
1717
+ res = cordisOriginal.readImage.call(cordisOriginal, cand);
1718
+ }
1719
+ }
1720
+ if (res && typeof res === 'object') {
1721
+ // If it's a Promise (async), safely attach catch to avoid unhandledRejection crashes
1722
+ if (typeof res.then === 'function') {
1723
+ if (typeof res.catch === 'function') {
1724
+ res.catch(() => { });
1725
+ }
1726
+ }
1727
+ else {
1728
+ if (res instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(res))) {
1729
+ return res;
1730
+ }
1731
+ const data = res.data;
1732
+ if (data instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) || typeof data === 'string') {
1733
+ return { data };
1734
+ }
1735
+ }
1736
+ }
1737
+ }
1738
+ catch { }
1739
+ }
1740
+ }
1741
+ // D. readAttachment
1742
+ if (typeof store.readAttachment === 'function') {
1743
+ try {
1744
+ let res;
1745
+ try {
1746
+ res = store.readAttachment.call(store, id);
1747
+ }
1748
+ catch {
1749
+ if (cordisOriginal && typeof cordisOriginal.readAttachment === 'function') {
1750
+ res = cordisOriginal.readAttachment.call(cordisOriginal, id);
1751
+ }
1752
+ }
1753
+ if (res && typeof res.then === 'function') {
1754
+ if (typeof res.catch === 'function') {
1755
+ res.catch(() => { });
1756
+ }
1757
+ }
1758
+ else if (res !== undefined) {
1759
+ return res;
1760
+ }
1761
+ }
1762
+ catch { }
1763
+ }
1764
+ // E. getAttachment
1765
+ if (typeof store.getAttachment === 'function') {
1766
+ try {
1767
+ let res;
1768
+ try {
1769
+ res = store.getAttachment.call(store, id);
1770
+ }
1771
+ catch {
1772
+ if (cordisOriginal && typeof cordisOriginal.getAttachment === 'function') {
1773
+ res = cordisOriginal.getAttachment.call(cordisOriginal, id);
1774
+ }
1775
+ }
1776
+ if (res && typeof res.then === 'function') {
1777
+ if (typeof res.catch === 'function') {
1778
+ res.catch(() => { });
1779
+ }
1780
+ }
1781
+ else if (res !== undefined) {
1782
+ return res;
1783
+ }
1784
+ }
1785
+ catch { }
1786
+ }
1787
+ return undefined;
1788
+ }
1789
+ function tryStoreFreeDiskAttachment(id, context) {
1790
+ if (typeof id !== 'string')
1791
+ return undefined;
1792
+ let hash;
1793
+ const trimmed = id.trim();
1794
+ if (trimmed.startsWith('sha256:')) {
1795
+ hash = trimmed.slice('sha256:'.length).toLowerCase();
1796
+ }
1797
+ else if (/^[0-9a-f]{64}$/i.test(trimmed)) {
1798
+ hash = trimmed.toLowerCase();
1799
+ }
1800
+ if (!hash || hash.length < 2)
1801
+ return undefined;
1802
+ const prefix = hash.slice(0, 2);
1803
+ const relPaths = [
1804
+ `attachments/v1/objects/${prefix}/${hash}`,
1805
+ `attachments/v1/file-objects/${prefix}/${hash}`,
1806
+ ];
1807
+ const candidatePaths = [];
1808
+ if (typeof process !== 'undefined' && process.env) {
1809
+ const env = process.env;
1810
+ if (env.DSH_NTR_STATE_DIR) {
1811
+ const stateDir = env.DSH_NTR_STATE_DIR.replaceAll('\\', '/').replace(/\/+$/, '');
1812
+ const hostDir = stateDir.endsWith('/dsh-ntr') ? stateDir.slice(0, -'/dsh-ntr'.length) : stateDir;
1813
+ for (const rel of relPaths)
1814
+ candidatePaths.push(`${hostDir}/${rel}`);
1815
+ }
1816
+ if (env.DSH_DIR) {
1817
+ const dshDir = env.DSH_DIR.replaceAll('\\', '/').replace(/\/+$/, '');
1818
+ for (const rel of relPaths)
1819
+ candidatePaths.push(`${dshDir}/${rel}`);
1820
+ }
1821
+ const home = env.HOME || env.USERPROFILE || (env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : undefined);
1822
+ if (home) {
1823
+ const cleanHome = home.replaceAll('\\', '/').replace(/\/+$/, '');
1824
+ for (const rel of relPaths) {
1825
+ candidatePaths.push(`${cleanHome}/.dsh/${rel}`);
1826
+ candidatePaths.push(`${cleanHome}\\.dsh\\${rel.replaceAll('/', '\\')}`);
1827
+ }
1828
+ }
1829
+ }
1830
+ for (const rel of relPaths)
1831
+ candidatePaths.push(`~/.dsh/${rel}`);
1832
+ for (const p of candidatePaths) {
1833
+ const fileContent = readImageFileFromContext(p, context);
1834
+ if (fileContent !== undefined) {
1835
+ return {
1836
+ path: p,
1837
+ data: fileContent,
1838
+ };
1839
+ }
1840
+ }
1841
+ return undefined;
1842
+ }
1843
+ export function resolveImageAttachmentFromContext(id, context, attachmentMeta, rootContext) {
1844
+ const root = rootContext ?? context;
1845
+ if (context && typeof context === 'object') {
1846
+ const ctx = context;
1847
+ const direct = tryResolveFromAttachmentStore(ctx, id, root, attachmentMeta);
1848
+ if (direct !== undefined)
1849
+ return direct;
1850
+ if (typeof ctx.get === 'function') {
1851
+ try {
1852
+ const svc = ctx.get('attachments') ?? ctx.get('attachmentService');
1853
+ if (svc && typeof svc === 'object') {
1854
+ const res = resolveImageAttachmentFromContext(id, svc, attachmentMeta, root);
1855
+ if (res !== undefined)
1856
+ return res;
1857
+ }
1858
+ }
1859
+ catch { }
1860
+ }
1861
+ try {
1862
+ if (ctx.attachmentService && typeof ctx.attachmentService === 'object') {
1863
+ const res = resolveImageAttachmentFromContext(id, ctx.attachmentService, attachmentMeta, root);
1864
+ if (res !== undefined)
1865
+ return res;
1866
+ }
1867
+ }
1868
+ catch { }
1869
+ try {
1870
+ const attStore = ctx.attachments;
1871
+ if (attStore) {
1872
+ if (typeof attStore === 'object') {
1873
+ const fromStore = tryResolveFromAttachmentStore(attStore, id, root, attachmentMeta);
1874
+ if (fromStore !== undefined)
1875
+ return fromStore;
1876
+ }
1877
+ if (attStore instanceof Map || typeof attStore.get === 'function') {
1878
+ try {
1879
+ return attStore.get(id);
1880
+ }
1881
+ catch { }
1882
+ }
1883
+ if (typeof attStore === 'object' && Object.prototype.hasOwnProperty.call(attStore, id)) {
1884
+ return attStore[id];
1885
+ }
1886
+ }
1887
+ }
1888
+ catch { }
1889
+ }
1890
+ return tryStoreFreeDiskAttachment(id, root);
1891
+ }
1892
+ function toBase64(data) {
1893
+ if (typeof data === 'string')
1894
+ return data.replace(/\s+/g, '');
1895
+ if (typeof Buffer !== 'undefined')
1896
+ return Buffer.from(data).toString('base64');
1897
+ let binary = '';
1898
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
1899
+ for (let i = 0; i < bytes.byteLength; i++) {
1900
+ binary += String.fromCharCode(bytes[i]);
1901
+ }
1902
+ return btoa(binary);
1903
+ }
1904
+ /**
1905
+ * Extract an image payload (Base64 data + MIME type) from an image block and optional context.
1906
+ */
1907
+ export function resolveImageContent(block, context) {
1908
+ if (!block || typeof block !== 'object')
1909
+ return undefined;
1910
+ const b = block;
1911
+ const attRecord = b.attachment && typeof b.attachment === 'object' ? b.attachment : undefined;
1912
+ const isExplicitImageBlock = b.type === 'image' || b.type === 'image_url' || b.type === 'attachment';
1913
+ const explicitMime = (typeof attRecord?.mediaType === 'string' && attRecord.mediaType.trim().toLowerCase()) ||
1914
+ (typeof attRecord?.mimeType === 'string' && attRecord.mimeType.trim().toLowerCase()) ||
1915
+ (typeof b.mediaType === 'string' && b.mediaType.trim().toLowerCase()) ||
1916
+ (typeof b.mimeType === 'string' && b.mimeType.trim().toLowerCase()) ||
1917
+ (typeof b.contentType === 'string' && b.contentType.trim().toLowerCase()) ||
1918
+ undefined;
1919
+ // 1. Direct base64 data: { data: string | Buffer | Uint8Array, mimeType?: string }
1920
+ if (typeof b.data === 'string' && b.data.trim() !== '') {
1921
+ if (b.data.startsWith('data:')) {
1922
+ const parsed = parseImageDataUrl(b.data);
1923
+ if (parsed) {
1924
+ const mimeType = explicitMime || parsed.mimeType || detectImageMimeFromMagic(parsed.data) || 'image/png';
1925
+ return { type: 'image', data: parsed.data, mimeType, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) };
1926
+ }
1927
+ return undefined;
1928
+ }
1929
+ const mimeType = explicitMime || detectImageMimeFromMagic(b.data) || (isExplicitImageBlock ? 'image/png' : undefined);
1930
+ if (mimeType && mimeType.startsWith('image/')) {
1931
+ return {
1932
+ type: 'image',
1933
+ data: b.data.trim().replace(/\s+/g, ''),
1934
+ mimeType,
1935
+ ...(typeof b.uri === 'string' ? { uri: b.uri } : {}),
1936
+ };
1937
+ }
1938
+ }
1939
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(b.data)) {
1940
+ const mimeType = explicitMime || detectImageMimeFromMagic(b.data) || (isExplicitImageBlock ? 'image/png' : undefined);
1941
+ if (mimeType && mimeType.startsWith('image/')) {
1942
+ return { type: 'image', data: b.data.toString('base64'), mimeType, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) };
1943
+ }
1944
+ }
1945
+ if (b.data instanceof Uint8Array) {
1946
+ const mimeType = explicitMime || detectImageMimeFromMagic(b.data) || (isExplicitImageBlock ? 'image/png' : undefined);
1947
+ if (mimeType && mimeType.startsWith('image/')) {
1948
+ return { type: 'image', data: toBase64(b.data), mimeType, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) };
1949
+ }
1950
+ }
1951
+ // 2. Anthropic style: source: { type: 'base64', media_type: string, data: string }
1952
+ if (b.source && typeof b.source === 'object') {
1953
+ const src = b.source;
1954
+ const srcMime = (typeof src.media_type === 'string' && src.media_type.trim().toLowerCase()) ||
1955
+ (typeof src.mimeType === 'string' && src.mimeType.trim().toLowerCase()) ||
1956
+ explicitMime ||
1957
+ (typeof src.data === 'string' || src.data instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(src.data)) ? detectImageMimeFromMagic(src.data) : undefined) ||
1958
+ 'image/png';
1959
+ if (srcMime.startsWith('image/')) {
1960
+ if (typeof src.data === 'string' && src.data.trim() !== '') {
1961
+ return {
1962
+ type: 'image',
1963
+ data: src.data.trim().replace(/\s+/g, ''),
1964
+ mimeType: srcMime,
1965
+ ...(typeof b.uri === 'string' ? { uri: b.uri } : {}),
1966
+ };
1967
+ }
1968
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(src.data)) {
1969
+ return { type: 'image', data: src.data.toString('base64'), mimeType: srcMime, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) };
1970
+ }
1971
+ if (src.data instanceof Uint8Array) {
1972
+ return { type: 'image', data: toBase64(src.data), mimeType: srcMime, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) };
1973
+ }
1974
+ }
1975
+ }
1976
+ // 3. Image property: { image: string | Buffer | Uint8Array }
1977
+ if (typeof b.image === 'string' && b.image.trim() !== '') {
1978
+ if (b.image.startsWith('data:')) {
1979
+ const parsed = parseImageDataUrl(b.image);
1980
+ if (parsed)
1981
+ return { type: 'image', data: parsed.data, mimeType: explicitMime || parsed.mimeType || detectImageMimeFromMagic(parsed.data) || 'image/png' };
1982
+ return undefined;
1983
+ }
1984
+ const mimeType = explicitMime || detectImageMimeFromMagic(b.image) || (isExplicitImageBlock ? 'image/png' : undefined);
1985
+ if (mimeType && mimeType.startsWith('image/')) {
1986
+ return { type: 'image', data: b.image.trim().replace(/\s+/g, ''), mimeType };
1987
+ }
1988
+ }
1989
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(b.image)) {
1990
+ const mimeType = explicitMime || detectImageMimeFromMagic(b.image) || (isExplicitImageBlock ? 'image/png' : undefined);
1991
+ if (mimeType && mimeType.startsWith('image/'))
1992
+ return { type: 'image', data: b.image.toString('base64'), mimeType };
1993
+ }
1994
+ if (b.image instanceof Uint8Array) {
1995
+ const mimeType = explicitMime || detectImageMimeFromMagic(b.image) || (isExplicitImageBlock ? 'image/png' : undefined);
1996
+ if (mimeType && mimeType.startsWith('image/'))
1997
+ return { type: 'image', data: toBase64(b.image), mimeType };
1998
+ }
1999
+ // 4. OpenAI style image_url: { image_url: string | { url: string } }
2000
+ if (b.image_url) {
2001
+ const urlStr = typeof b.image_url === 'string'
2002
+ ? b.image_url
2003
+ : (typeof b.image_url === 'object' && b.image_url !== null ? b.image_url.url : undefined);
2004
+ if (typeof urlStr === 'string' && urlStr.trim() !== '') {
2005
+ if (urlStr.startsWith('data:')) {
2006
+ const parsed = parseImageDataUrl(urlStr);
2007
+ if (parsed)
2008
+ return { type: 'image', data: parsed.data, mimeType: explicitMime || parsed.mimeType || detectImageMimeFromMagic(parsed.data) || 'image/png', uri: urlStr };
2009
+ return undefined;
2010
+ }
2011
+ if (urlStr.startsWith('http://') || urlStr.startsWith('https://')) {
2012
+ const mimeType = explicitMime || guessImageMimeType(urlStr) || 'image/png';
2013
+ return { type: 'image', data: '', mimeType, uri: urlStr };
2014
+ }
2015
+ if (urlStr.startsWith('file://') || isLocalFilePath(urlStr)) {
2016
+ const fileContent = readImageFileFromContext(urlStr, context);
2017
+ if (fileContent !== undefined) {
2018
+ let data;
2019
+ let resolvedMime;
2020
+ if (typeof fileContent === 'string') {
2021
+ if (fileContent.startsWith('data:')) {
2022
+ const parsed = parseImageDataUrl(fileContent);
2023
+ if (parsed) {
2024
+ data = parsed.data;
2025
+ resolvedMime = parsed.mimeType;
2026
+ }
2027
+ else {
2028
+ return undefined;
2029
+ }
2030
+ }
2031
+ else {
2032
+ data = fileContent.trim().replace(/\s+/g, '');
2033
+ }
2034
+ }
2035
+ else {
2036
+ data = toBase64(fileContent);
2037
+ }
2038
+ const mimeType = explicitMime || resolvedMime || guessImageMimeType(urlStr) || detectImageMimeFromMagic(fileContent) || (isExplicitImageBlock ? 'image/png' : undefined);
2039
+ if (mimeType && mimeType.startsWith('image/')) {
2040
+ return { type: 'image', data, mimeType, uri: urlStr };
2041
+ }
2042
+ }
2043
+ }
2044
+ }
2045
+ }
2046
+ // 5. Direct buffer / content on block: { buffer: Buffer } or { content: Buffer }
2047
+ if (b.buffer && (typeof Buffer !== 'undefined' && Buffer.isBuffer(b.buffer) || b.buffer instanceof Uint8Array)) {
2048
+ const data = typeof Buffer !== 'undefined' && Buffer.isBuffer(b.buffer) ? b.buffer.toString('base64') : toBase64(b.buffer);
2049
+ const mimeType = explicitMime || detectImageMimeFromMagic(b.buffer) || (isExplicitImageBlock ? 'image/png' : undefined);
2050
+ if (mimeType && mimeType.startsWith('image/')) {
2051
+ return { type: 'image', data, mimeType, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) };
2052
+ }
2053
+ }
2054
+ if (b.content && (typeof Buffer !== 'undefined' && Buffer.isBuffer(b.content) || b.content instanceof Uint8Array)) {
2055
+ const data = typeof Buffer !== 'undefined' && Buffer.isBuffer(b.content) ? b.content.toString('base64') : toBase64(b.content);
2056
+ const mimeType = explicitMime || detectImageMimeFromMagic(b.content) || (isExplicitImageBlock ? 'image/png' : undefined);
2057
+ if (mimeType && mimeType.startsWith('image/')) {
2058
+ return { type: 'image', data, mimeType, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) };
2059
+ }
2060
+ }
2061
+ // 6. Data URL or Remote URL in `url`
2062
+ if (typeof b.url === 'string') {
2063
+ if (b.url.startsWith('data:')) {
2064
+ const parsed = parseImageDataUrl(b.url);
2065
+ if (parsed)
2066
+ return { type: 'image', data: parsed.data, mimeType: explicitMime || parsed.mimeType || detectImageMimeFromMagic(parsed.data) || 'image/png', uri: b.url };
2067
+ return undefined;
2068
+ }
2069
+ if (b.url.startsWith('http://') || b.url.startsWith('https://')) {
2070
+ const mimeType = explicitMime || guessImageMimeType(b.url) || 'image/png';
2071
+ return { type: 'image', data: '', mimeType, uri: b.url };
2072
+ }
2073
+ }
2074
+ // 7. File path in `path`, `filePath`, or `url: file://...`
2075
+ let filePath;
2076
+ if (typeof b.path === 'string' && b.path !== '')
2077
+ filePath = b.path;
2078
+ else if (typeof b.filePath === 'string' && b.filePath !== '')
2079
+ filePath = b.filePath;
2080
+ else if (typeof b.url === 'string' && (b.url.startsWith('file://') || isLocalFilePath(b.url)))
2081
+ filePath = b.url;
2082
+ if (filePath !== undefined) {
2083
+ const fileContent = readImageFileFromContext(filePath, context);
2084
+ if (fileContent !== undefined) {
2085
+ let data;
2086
+ let resolvedMime;
2087
+ if (typeof fileContent === 'string') {
2088
+ if (fileContent.startsWith('data:')) {
2089
+ const parsed = parseImageDataUrl(fileContent);
2090
+ if (parsed) {
2091
+ data = parsed.data;
2092
+ resolvedMime = parsed.mimeType;
2093
+ }
2094
+ else {
2095
+ return undefined;
2096
+ }
2097
+ }
2098
+ else {
2099
+ data = fileContent.trim().replace(/\s+/g, '');
2100
+ }
2101
+ }
2102
+ else {
2103
+ data = toBase64(fileContent);
2104
+ }
2105
+ const mimeType = explicitMime ||
2106
+ resolvedMime ||
2107
+ guessImageMimeType(filePath) ||
2108
+ detectImageMimeFromMagic(fileContent) ||
2109
+ (isExplicitImageBlock ? 'image/png' : undefined);
2110
+ if (mimeType && mimeType.startsWith('image/')) {
2111
+ let uri = typeof b.url === 'string' ? b.url : undefined;
2112
+ if (!uri) {
2113
+ if (filePath.startsWith('file://')) {
2114
+ uri = filePath;
2115
+ }
2116
+ else if (isAbsolutePath(filePath)) {
2117
+ if (filePath.startsWith('//') || filePath.startsWith('\\\\')) {
2118
+ uri = `file://${filePath.replace(/^[\/\\]+/, '').replace(/\\/g, '/')}`;
2119
+ }
2120
+ else if (/^[a-zA-Z]:/.test(filePath)) {
2121
+ uri = `file:///${filePath.replace(/\\/g, '/')}`;
2122
+ }
2123
+ else {
2124
+ uri = `file://${filePath.startsWith('/') ? filePath : `/${filePath}`}`;
2125
+ }
2126
+ }
2127
+ else {
2128
+ uri = filePath;
2129
+ }
2130
+ }
2131
+ return {
2132
+ type: 'image',
2133
+ data,
2134
+ mimeType,
2135
+ uri,
2136
+ };
2137
+ }
2138
+ }
2139
+ }
2140
+ // 8. Attachment resolution via context
2141
+ let attachmentId;
2142
+ if (attRecord && typeof attRecord.attachmentId === 'string' && attRecord.attachmentId.trim() !== '') {
2143
+ attachmentId = attRecord.attachmentId.trim();
2144
+ }
2145
+ else if (typeof b.attachmentId === 'string' && b.attachmentId.trim() !== '') {
2146
+ attachmentId = b.attachmentId.trim();
2147
+ }
2148
+ else if (attRecord && typeof attRecord.id === 'string' && attRecord.id.trim() !== '') {
2149
+ attachmentId = attRecord.id.trim();
2150
+ }
2151
+ else if (typeof b.id === 'string' && b.id.trim() !== '') {
2152
+ attachmentId = b.id.trim();
2153
+ }
2154
+ else if (typeof b.attachment === 'string' && b.attachment.trim() !== '') {
2155
+ attachmentId = b.attachment.trim();
2156
+ }
2157
+ else if (typeof b.url === 'string' && b.url.startsWith('attachment:')) {
2158
+ attachmentId = b.url.replace(/^attachment:\/\/|^attachment:/, '');
2159
+ }
2160
+ else if (typeof b.image_url === 'string' && b.image_url.startsWith('attachment:')) {
2161
+ attachmentId = b.image_url.replace(/^attachment:\/\/|^attachment:/, '');
2162
+ }
2163
+ else if (b.image_url && typeof b.image_url === 'object' && typeof b.image_url.url === 'string' && b.image_url.url.startsWith('attachment:')) {
2164
+ attachmentId = b.image_url.url.replace(/^attachment:\/\/|^attachment:/, '');
2165
+ }
2166
+ if (attachmentId !== undefined) {
2167
+ const rawAtt = resolveImageAttachmentFromContext(attachmentId, context, b.attachment);
2168
+ if (rawAtt !== undefined) {
2169
+ if (typeof rawAtt === 'string') {
2170
+ if (rawAtt.startsWith('data:')) {
2171
+ const parsed = parseImageDataUrl(rawAtt);
2172
+ if (parsed) {
2173
+ const mimeType = explicitMime || parsed.mimeType || detectImageMimeFromMagic(parsed.data) || 'image/png';
2174
+ return {
2175
+ type: 'image',
2176
+ data: parsed.data,
2177
+ mimeType,
2178
+ uri: typeof b.uri === 'string' ? b.uri : `attachment://${attachmentId}`,
2179
+ };
2180
+ }
2181
+ return undefined;
2182
+ }
2183
+ else if (rawAtt.startsWith('file://') || isLocalFilePath(rawAtt)) {
2184
+ const fileContent = readImageFileFromContext(rawAtt, context);
2185
+ if (fileContent !== undefined) {
2186
+ let data;
2187
+ let resolvedMime;
2188
+ if (typeof fileContent === 'string') {
2189
+ if (fileContent.startsWith('data:')) {
2190
+ const parsed = parseImageDataUrl(fileContent);
2191
+ if (parsed) {
2192
+ data = parsed.data;
2193
+ resolvedMime = parsed.mimeType;
2194
+ }
2195
+ else {
2196
+ return undefined;
2197
+ }
2198
+ }
2199
+ else {
2200
+ data = fileContent.trim().replace(/\s+/g, '');
2201
+ }
2202
+ }
2203
+ else {
2204
+ data = toBase64(fileContent);
2205
+ }
2206
+ const mimeType = explicitMime ||
2207
+ resolvedMime ||
2208
+ (typeof attRecord?.name === 'string' ? guessImageMimeType(attRecord.name) : undefined) ||
2209
+ guessImageMimeType(rawAtt) ||
2210
+ detectImageMimeFromMagic(fileContent) ||
2211
+ (isExplicitImageBlock ? 'image/png' : undefined);
2212
+ if (mimeType && mimeType.startsWith('image/')) {
2213
+ return { type: 'image', data, mimeType, uri: typeof b.uri === 'string' ? b.uri : `attachment://${attachmentId}` };
2214
+ }
2215
+ }
2216
+ }
2217
+ const mimeType = explicitMime ||
2218
+ (typeof attRecord?.name === 'string' ? guessImageMimeType(attRecord.name) : undefined) ||
2219
+ guessImageMimeType(attachmentId) ||
2220
+ detectImageMimeFromMagic(rawAtt) ||
2221
+ (isExplicitImageBlock ? 'image/png' : undefined);
2222
+ if (mimeType && mimeType.startsWith('image/')) {
2223
+ return { type: 'image', data: rawAtt.trim().replace(/\s+/g, ''), mimeType, uri: typeof b.uri === 'string' ? b.uri : `attachment://${attachmentId}` };
2224
+ }
2225
+ }
2226
+ if (typeof rawAtt === 'object' && rawAtt !== null) {
2227
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(rawAtt)) {
2228
+ const mimeType = explicitMime ||
2229
+ (typeof attRecord?.name === 'string' ? guessImageMimeType(attRecord.name) : undefined) ||
2230
+ guessImageMimeType(attachmentId) ||
2231
+ detectImageMimeFromMagic(rawAtt) ||
2232
+ (isExplicitImageBlock ? 'image/png' : undefined);
2233
+ if (mimeType && mimeType.startsWith('image/')) {
2234
+ return {
2235
+ type: 'image',
2236
+ data: rawAtt.toString('base64'),
2237
+ mimeType,
2238
+ uri: typeof b.uri === 'string' ? b.uri : `attachment://${attachmentId}`,
2239
+ };
2240
+ }
2241
+ }
2242
+ if (rawAtt instanceof Uint8Array) {
2243
+ const mimeType = explicitMime ||
2244
+ (typeof attRecord?.name === 'string' ? guessImageMimeType(attRecord.name) : undefined) ||
2245
+ guessImageMimeType(attachmentId) ||
2246
+ detectImageMimeFromMagic(rawAtt) ||
2247
+ (isExplicitImageBlock ? 'image/png' : undefined);
2248
+ if (mimeType && mimeType.startsWith('image/')) {
2249
+ return {
2250
+ type: 'image',
2251
+ data: toBase64(rawAtt),
2252
+ mimeType,
2253
+ uri: typeof b.uri === 'string' ? b.uri : `attachment://${attachmentId}`,
2254
+ };
2255
+ }
2256
+ }
2257
+ const attObj = rawAtt;
2258
+ let data;
2259
+ let resolvedMime;
2260
+ const attPath = typeof attObj.path === 'string' && attObj.path !== ''
2261
+ ? attObj.path
2262
+ : (typeof attObj.filePath === 'string' && attObj.filePath !== ''
2263
+ ? attObj.filePath
2264
+ : (typeof attObj.url === 'string' && (attObj.url.startsWith('file://') || isLocalFilePath(attObj.url)) ? attObj.url : undefined));
2265
+ let fileContentForMagic;
2266
+ if (attPath !== undefined) {
2267
+ const fileContent = readImageFileFromContext(attPath, context);
2268
+ if (fileContent !== undefined) {
2269
+ fileContentForMagic = fileContent;
2270
+ if (typeof fileContent === 'string') {
2271
+ if (fileContent.startsWith('data:')) {
2272
+ const parsed = parseImageDataUrl(fileContent);
2273
+ if (parsed) {
2274
+ data = parsed.data;
2275
+ resolvedMime = parsed.mimeType;
2276
+ }
2277
+ }
2278
+ else {
2279
+ data = fileContent.trim().replace(/\s+/g, '');
2280
+ }
2281
+ }
2282
+ else {
2283
+ data = toBase64(fileContent);
2284
+ }
2285
+ if (!resolvedMime)
2286
+ resolvedMime = guessImageMimeType(attPath);
2287
+ }
2288
+ }
2289
+ if (data === undefined) {
2290
+ if (typeof attObj.data === 'string' && attObj.data.trim() !== '') {
2291
+ if (attObj.data.startsWith('data:')) {
2292
+ const parsed = parseImageDataUrl(attObj.data);
2293
+ if (parsed) {
2294
+ data = parsed.data;
2295
+ resolvedMime = parsed.mimeType;
2296
+ }
2297
+ }
2298
+ else {
2299
+ data = attObj.data.trim().replace(/\s+/g, '');
2300
+ fileContentForMagic = attObj.data;
2301
+ }
2302
+ }
2303
+ else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(attObj.data)) {
2304
+ data = attObj.data.toString('base64');
2305
+ fileContentForMagic = attObj.data;
2306
+ }
2307
+ else if (attObj.data instanceof Uint8Array) {
2308
+ data = toBase64(attObj.data);
2309
+ fileContentForMagic = attObj.data;
2310
+ }
2311
+ else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(attObj.buffer)) {
2312
+ data = attObj.buffer.toString('base64');
2313
+ fileContentForMagic = attObj.buffer;
2314
+ }
2315
+ else if (attObj.buffer instanceof Uint8Array) {
2316
+ data = toBase64(attObj.buffer);
2317
+ fileContentForMagic = attObj.buffer;
2318
+ }
2319
+ else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(attObj.content)) {
2320
+ data = attObj.content.toString('base64');
2321
+ fileContentForMagic = attObj.content;
2322
+ }
2323
+ else if (attObj.content instanceof Uint8Array) {
2324
+ data = toBase64(attObj.content);
2325
+ fileContentForMagic = attObj.content;
2326
+ }
2327
+ else if (typeof attObj.content === 'string') {
2328
+ if (attObj.content.startsWith('data:')) {
2329
+ const parsed = parseImageDataUrl(attObj.content);
2330
+ if (parsed) {
2331
+ data = parsed.data;
2332
+ resolvedMime = parsed.mimeType;
2333
+ }
2334
+ }
2335
+ else {
2336
+ data = attObj.content.trim().replace(/\s+/g, '');
2337
+ fileContentForMagic = attObj.content;
2338
+ }
2339
+ }
2340
+ }
2341
+ if (data !== undefined) {
2342
+ const mimeType = explicitMime ||
2343
+ (typeof attObj.mediaType === 'string' && attObj.mediaType.toLowerCase()) ||
2344
+ (typeof attObj.mimeType === 'string' && attObj.mimeType.toLowerCase()) ||
2345
+ resolvedMime ||
2346
+ (typeof attRecord?.name === 'string' ? guessImageMimeType(attRecord.name) : undefined) ||
2347
+ (typeof attObj.name === 'string' ? guessImageMimeType(attObj.name) : undefined) ||
2348
+ guessImageMimeType(attachmentId) ||
2349
+ detectImageMimeFromMagic(fileContentForMagic ?? data) ||
2350
+ (isExplicitImageBlock ? 'image/png' : undefined);
2351
+ if (mimeType && mimeType.startsWith('image/')) {
2352
+ return {
2353
+ type: 'image',
2354
+ data,
2355
+ mimeType,
2356
+ uri: typeof b.uri === 'string' ? b.uri : `attachment://${attachmentId}`,
2357
+ };
2358
+ }
2359
+ }
2360
+ }
2361
+ }
2362
+ }
2363
+ // 9. Direct b.attachment object
2364
+ if (b.attachment && typeof b.attachment === 'object') {
2365
+ const attObj = b.attachment;
2366
+ let data;
2367
+ let resolvedMime;
2368
+ const attPath = typeof attObj.path === 'string' && attObj.path !== ''
2369
+ ? attObj.path
2370
+ : (typeof attObj.filePath === 'string' && attObj.filePath !== ''
2371
+ ? attObj.filePath
2372
+ : (typeof attObj.url === 'string' && (attObj.url.startsWith('file://') || isLocalFilePath(attObj.url)) ? attObj.url : undefined));
2373
+ let fileContentForMagic;
2374
+ if (attPath !== undefined) {
2375
+ const fileContent = readImageFileFromContext(attPath, context);
2376
+ if (fileContent !== undefined) {
2377
+ fileContentForMagic = fileContent;
2378
+ if (typeof fileContent === 'string') {
2379
+ if (fileContent.startsWith('data:')) {
2380
+ const parsed = parseImageDataUrl(fileContent);
2381
+ if (parsed) {
2382
+ data = parsed.data;
2383
+ resolvedMime = parsed.mimeType;
2384
+ }
2385
+ }
2386
+ else {
2387
+ data = fileContent.trim().replace(/\s+/g, '');
2388
+ }
2389
+ }
2390
+ else {
2391
+ data = toBase64(fileContent);
2392
+ }
2393
+ if (!resolvedMime)
2394
+ resolvedMime = guessImageMimeType(attPath);
2395
+ }
2396
+ }
2397
+ if (data === undefined) {
2398
+ if (typeof attObj.data === 'string' && attObj.data.trim() !== '') {
2399
+ if (attObj.data.startsWith('data:')) {
2400
+ const parsed = parseImageDataUrl(attObj.data);
2401
+ if (parsed) {
2402
+ data = parsed.data;
2403
+ resolvedMime = parsed.mimeType;
2404
+ }
2405
+ }
2406
+ else {
2407
+ data = attObj.data.trim().replace(/\s+/g, '');
2408
+ fileContentForMagic = attObj.data;
2409
+ }
2410
+ }
2411
+ else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(attObj.data)) {
2412
+ data = attObj.data.toString('base64');
2413
+ fileContentForMagic = attObj.data;
2414
+ }
2415
+ else if (attObj.data instanceof Uint8Array) {
2416
+ data = toBase64(attObj.data);
2417
+ fileContentForMagic = attObj.data;
2418
+ }
2419
+ else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(attObj.buffer)) {
2420
+ data = attObj.buffer.toString('base64');
2421
+ fileContentForMagic = attObj.buffer;
2422
+ }
2423
+ else if (attObj.buffer instanceof Uint8Array) {
2424
+ data = toBase64(attObj.buffer);
2425
+ fileContentForMagic = attObj.buffer;
2426
+ }
2427
+ else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(attObj.content)) {
2428
+ data = attObj.content.toString('base64');
2429
+ fileContentForMagic = attObj.content;
2430
+ }
2431
+ else if (attObj.content instanceof Uint8Array) {
2432
+ data = toBase64(attObj.content);
2433
+ fileContentForMagic = attObj.content;
2434
+ }
2435
+ else if (typeof attObj.content === 'string') {
2436
+ if (attObj.content.startsWith('data:')) {
2437
+ const parsed = parseImageDataUrl(attObj.content);
2438
+ if (parsed) {
2439
+ data = parsed.data;
2440
+ resolvedMime = parsed.mimeType;
2441
+ }
2442
+ }
2443
+ else {
2444
+ data = attObj.content.trim().replace(/\s+/g, '');
2445
+ fileContentForMagic = attObj.content;
2446
+ }
2447
+ }
2448
+ }
2449
+ if (data !== undefined) {
2450
+ const mimeType = explicitMime ||
2451
+ (typeof attObj.mediaType === 'string' && attObj.mediaType.toLowerCase()) ||
2452
+ (typeof attObj.mimeType === 'string' && attObj.mimeType.toLowerCase()) ||
2453
+ resolvedMime ||
2454
+ (typeof attObj.name === 'string' ? guessImageMimeType(attObj.name) : undefined) ||
2455
+ (attPath ? guessImageMimeType(attPath) : undefined) ||
2456
+ detectImageMimeFromMagic(fileContentForMagic ?? data) ||
2457
+ (isExplicitImageBlock ? 'image/png' : undefined);
2458
+ if (mimeType && mimeType.startsWith('image/')) {
2459
+ return { type: 'image', data, mimeType, ...(typeof b.uri === 'string' ? { uri: b.uri } : {}) };
2460
+ }
2461
+ }
2462
+ }
2463
+ return undefined;
2464
+ }
1313
2465
  /**
1314
2466
  * Select the user's own words out of a host-built request.
1315
2467
  *
@@ -1323,9 +2475,10 @@ export function describeUnhandledUpdate(kind, update) {
1323
2475
  * (tests, headless callers) and counts as user input.
1324
2476
  *
1325
2477
  * @param messages - the request's ordered messages, exactly as the host built them.
1326
- * @returns the last real user message's text, or why there is none to forward.
2478
+ * @param context - optional host context or attachment resolver for reading attachments.
2479
+ * @returns the last real user message's text and any image content blocks, or why there is none to forward.
1327
2480
  */
1328
- export function selectForwardPrompt(messages) {
2481
+ export function selectForwardPrompt(messages, context) {
1329
2482
  let sawUserRole = false;
1330
2483
  let last;
1331
2484
  for (const message of messages) {
@@ -1335,14 +2488,43 @@ export function selectForwardPrompt(messages) {
1335
2488
  const kind = message.source?.kind;
1336
2489
  if (kind !== undefined && kind !== 'user')
1337
2490
  continue;
1338
- const text = (message.content ?? [])
2491
+ const content = message.content ?? message.parts;
2492
+ const rawBlocks = Array.isArray(content)
2493
+ ? content
2494
+ : (typeof content === 'string'
2495
+ ? [{ type: 'text', text: content }]
2496
+ : (typeof content === 'object' && content !== null ? [content] : []));
2497
+ const text = rawBlocks
1339
2498
  .filter(block => block.type === 'text' && typeof block.text === 'string')
1340
2499
  .map(block => block.text)
1341
2500
  .join('\n')
1342
2501
  .trim();
1343
- if (text === '')
2502
+ const images = [];
2503
+ const contentBlocks = [];
2504
+ for (const block of rawBlocks) {
2505
+ if (!block || typeof block !== 'object')
2506
+ continue;
2507
+ if (block.type === 'text' && typeof block.text === 'string') {
2508
+ if (block.text.trim() !== '') {
2509
+ contentBlocks.push({ type: 'text', text: block.text });
2510
+ }
2511
+ }
2512
+ else {
2513
+ const resolved = resolveImageContent(block, context);
2514
+ if (resolved !== undefined) {
2515
+ images.push(resolved);
2516
+ contentBlocks.push(resolved);
2517
+ }
2518
+ }
2519
+ }
2520
+ if (text === '' && images.length === 0)
1344
2521
  continue;
1345
- last = { text, ...(message.id === undefined ? {} : { id: message.id }) };
2522
+ const item = { text, ...(message.id === undefined ? {} : { id: message.id }) };
2523
+ if (images.length > 0) {
2524
+ item.images = images;
2525
+ item.content = contentBlocks;
2526
+ }
2527
+ last = item;
1346
2528
  }
1347
2529
  if (last !== undefined)
1348
2530
  return { kind: 'prompt', ...last };
@@ -1469,7 +2651,36 @@ export function acpPresetStatusFromRoster(rows, id) {
1469
2651
  : { id, source: ACP_PRESET_SOURCE, installed: false, problem: broken };
1470
2652
  }
1471
2653
  // ---- Transcript ----
1472
- /** Bounded append-only log of raw ACP session updates with monotone seq. */
2654
+ /**
2655
+ * The kind of a state-only tick that consecutive repeats may share one entry.
2656
+ *
2657
+ * These are the frames the client folds to nothing (`STATE_ONLY_UPDATES`):
2658
+ * only their latest value is ever read. `session_info_update` qualifies only
2659
+ * while it carries no title — a title is content the replay must keep, so it
2660
+ * never lets a later tick replace its entry.
2661
+ */
2662
+ function coalescibleStateKind(update) {
2663
+ if (typeof update !== 'object' || update === null || Array.isArray(update))
2664
+ return undefined;
2665
+ const row = update;
2666
+ const kind = row.sessionUpdate;
2667
+ if (kind === 'usage_update' || kind === 'config_option_update')
2668
+ return kind;
2669
+ if (kind !== 'session_info_update')
2670
+ return undefined;
2671
+ const title = row.title;
2672
+ return title === undefined || title === null || title === '' ? kind : undefined;
2673
+ }
2674
+ /**
2675
+ * Bounded log of raw ACP session updates with monotone seq.
2676
+ *
2677
+ * Consecutive ticks of the same state-only kind share one entry (the entry is
2678
+ * replaced in place, not appended): chatty agents emit a usage / phase tick
2679
+ * every few hundred milliseconds, and letting each occupy the ring evicts the
2680
+ * conversation itself. This costs nothing on the live path — `append` still
2681
+ * allocates a fresh seq and the bridge broadcasts that frame — but replay via
2682
+ * `since` only sees the newest tick of each run.
2683
+ */
1473
2684
  export class Transcript {
1474
2685
  max;
1475
2686
  entries = [];
@@ -1479,6 +2690,16 @@ export class Transcript {
1479
2690
  }
1480
2691
  append(update) {
1481
2692
  const seq = this.next++;
2693
+ const last = this.entries[this.entries.length - 1];
2694
+ const kind = coalescibleStateKind(update);
2695
+ // Replace, don't push: `since(afterSeq)` keeps its filter, so intermediate
2696
+ // seq values are simply absent from replayed entries. Live SSE frames are
2697
+ // broadcast per append (the returned seq), and the client's live fold does
2698
+ // not depend on transcript contiguity — only `head` must keep advancing.
2699
+ if (kind !== undefined && last !== undefined && coalescibleStateKind(last.update) === kind) {
2700
+ this.entries[this.entries.length - 1] = { seq, update };
2701
+ return seq;
2702
+ }
1482
2703
  this.entries.push({ seq, update });
1483
2704
  if (this.entries.length > this.max)
1484
2705
  this.entries.splice(0, this.entries.length - this.max);
@@ -1673,7 +2894,44 @@ export async function dispatchRpc(manager, endpoint, payload) {
1673
2894
  case 'session/start':
1674
2895
  return ok(await manager.startSession(str(body, 'agentId', 64), optionalStr(body, 'cwd', 4096)));
1675
2896
  case 'session/prompt': {
1676
- const { turnId } = await manager.prompt(str(body, 'sessionId', 64), str(body, 'text', 1 << 20));
2897
+ let promptInput;
2898
+ if (Array.isArray(body.prompt)) {
2899
+ if (body.prompt.length === 0) {
2900
+ throw new AcpError('bad_payload', 'acp: prompt array cannot be empty');
2901
+ }
2902
+ for (const b of body.prompt) {
2903
+ if (!b || typeof b !== 'object')
2904
+ throw new AcpError('bad_payload', 'acp: prompt blocks must be objects');
2905
+ const block = b;
2906
+ if (block.type === 'text') {
2907
+ if (typeof block.text !== 'string')
2908
+ throw new AcpError('bad_payload', 'acp: text block requires string text');
2909
+ if (block.text.length > (1 << 20))
2910
+ throw new AcpError('bad_payload', 'acp: text block exceeds max length');
2911
+ }
2912
+ else if (block.type === 'image') {
2913
+ if (typeof block.data !== 'string' || typeof block.mimeType !== 'string') {
2914
+ throw new AcpError('bad_payload', 'acp: image block requires string data and mimeType');
2915
+ }
2916
+ if (!block.mimeType.toLowerCase().startsWith('image/')) {
2917
+ throw new AcpError('bad_payload', 'acp: image block mimeType must start with image/');
2918
+ }
2919
+ if (block.uri !== undefined && typeof block.uri !== 'string') {
2920
+ throw new AcpError('bad_payload', 'acp: image block uri must be string');
2921
+ }
2922
+ if (block.data.length > (20 << 20))
2923
+ throw new AcpError('bad_payload', 'acp: image block exceeds max data length');
2924
+ }
2925
+ else {
2926
+ throw new AcpError('bad_payload', `acp: unsupported prompt block type: ${String(block.type)}`);
2927
+ }
2928
+ }
2929
+ promptInput = body.prompt;
2930
+ }
2931
+ else {
2932
+ promptInput = str(body, 'text', 1 << 20);
2933
+ }
2934
+ const { turnId } = await manager.prompt(str(body, 'sessionId', 64), promptInput);
1677
2935
  return ok({ accepted: true, turnId });
1678
2936
  }
1679
2937
  case 'session/cancel':