@lotics/cli 0.152.1 → 0.152.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -44248,6 +44248,17 @@ function sessionId(now = Date.now(), env = process.env) {
44248
44248
  }
44249
44249
 
44250
44250
  // src/client.ts
44251
+ function parseContentDispositionFilename(disposition) {
44252
+ const extended = disposition.match(/filename\*=\s*([^']*)'[^']*'([^;\n]+)/i);
44253
+ if (extended) {
44254
+ try {
44255
+ return decodeURIComponent(extended[2].trim());
44256
+ } catch {
44257
+ }
44258
+ }
44259
+ const plain = disposition.match(/filename=\s*"?([^";\n]+)"?/i);
44260
+ return plain ? plain[1].trim() : null;
44261
+ }
44251
44262
  function findAvailableFilename(dir, filename, reserved) {
44252
44263
  const isTaken = (name2) => {
44253
44264
  const full = path2.join(dir, name2);
@@ -45115,8 +45126,7 @@ var LoticsClient = class {
45115
45126
  const response = await fetch(url2);
45116
45127
  if (!response.ok) await this.throwResponseError(response);
45117
45128
  const disposition = response.headers.get("content-disposition") ?? "";
45118
- const match2 = disposition.match(/filename="?([^";\n]+)"?/);
45119
- const originalFilename = match2?.[1] ?? fileId;
45129
+ const originalFilename = parseContentDispositionFilename(disposition) ?? fileId;
45120
45130
  const buffer = Buffer.from(await response.arrayBuffer());
45121
45131
  const dir = outputDir ? path2.resolve(outputDir) : process.cwd();
45122
45132
  await fs2.promises.mkdir(dir, { recursive: true });
@@ -97544,6 +97554,9 @@ function readChpx(grpprl) {
97544
97554
  case Sprm.CIstd:
97545
97555
  chp.charStyleIstd = s.operand.u16(0);
97546
97556
  break;
97557
+ case Sprm.CPicLocation:
97558
+ chp.picLocation = s.operand.u32(0);
97559
+ break;
97547
97560
  default:
97548
97561
  break;
97549
97562
  }
@@ -98494,9 +98507,93 @@ function applyCellPadding(operand, current) {
98494
98507
  return margins;
98495
98508
  }
98496
98509
 
98510
+ // ../ooxml/src/doc/pictures.ts
98511
+ var EMU_PER_TWIP = 635;
98512
+ var PICF_MIN_HEADER = 68;
98513
+ var PICMID_OFFSET = 28;
98514
+ var MM_SHAPE = 100;
98515
+ var BLIP_TYPES = {
98516
+ 61466: { mediaType: "image/x-emf", skipByInstance: { 980: 50, 981: 66 }, extractable: false },
98517
+ 61467: { mediaType: "image/x-wmf", skipByInstance: { 534: 50, 535: 66 }, extractable: false },
98518
+ 61468: { mediaType: "image/pict", skipByInstance: { 1346: 50, 1347: 66 }, extractable: false },
98519
+ 61469: { mediaType: "image/jpeg", skipByInstance: { 1130: 17, 1131: 33 }, extractable: true },
98520
+ 61470: { mediaType: "image/png", skipByInstance: { 1760: 17, 1761: 33 }, extractable: true },
98521
+ 61471: { mediaType: "image/bmp", skipByInstance: { 1960: 17, 1961: 33 }, extractable: false },
98522
+ 61481: { mediaType: "image/tiff", skipByInstance: { 1764: 17, 1765: 33 }, extractable: true },
98523
+ 61482: { mediaType: "image/jpeg", skipByInstance: { 1130: 17, 1131: 33 }, extractable: true }
98524
+ };
98525
+ function pictureExtension(mediaType) {
98526
+ switch (mediaType) {
98527
+ case "image/png":
98528
+ return "png";
98529
+ case "image/jpeg":
98530
+ return "jpeg";
98531
+ case "image/tiff":
98532
+ return "tiff";
98533
+ default:
98534
+ throw new Error(`No part extension for media type "${mediaType}"; it should not have been extracted`);
98535
+ }
98536
+ }
98537
+ function readInlinePicture(data2, fc) {
98538
+ if (fc < 0 || fc + PICF_MIN_HEADER > data2.length) return null;
98539
+ const lcb = data2.u32(fc);
98540
+ const cbHeader = data2.u16(fc + 4);
98541
+ const mm = data2.u16(fc + 6);
98542
+ if (cbHeader < PICF_MIN_HEADER || lcb <= cbHeader) return null;
98543
+ if (fc + lcb > data2.length) return null;
98544
+ const picmid = fc + PICMID_OFFSET;
98545
+ const dxaGoal = data2.u16(picmid);
98546
+ const dyaGoal = data2.u16(picmid + 2);
98547
+ const mx = data2.u16(picmid + 4) || 1e3;
98548
+ const my = data2.u16(picmid + 6) || 1e3;
98549
+ if (mm < MM_SHAPE) return null;
98550
+ const found = findBlip(data2.bytes.subarray(fc + cbHeader, fc + lcb));
98551
+ if (!found) return null;
98552
+ const widthEmu = Math.round(dxaGoal * EMU_PER_TWIP * mx / 1e3);
98553
+ const heightEmu = Math.round(dyaGoal * EMU_PER_TWIP * my / 1e3);
98554
+ if (widthEmu <= 0 || heightEmu <= 0) return null;
98555
+ return { bytes: found.bytes, mediaType: found.mediaType, widthEmu, heightEmu };
98556
+ }
98557
+ var BSE_RECORD = 61447;
98558
+ var BSE_HEADER = 36;
98559
+ var BSE_CB_NAME = 33;
98560
+ function findBlip(payload) {
98561
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
98562
+ let offset = 0;
98563
+ while (offset + 8 <= payload.length) {
98564
+ const verInstance = view.getUint16(offset, true);
98565
+ const recType = view.getUint16(offset + 2, true);
98566
+ const length2 = view.getUint32(offset + 4, true);
98567
+ const body = offset + 8;
98568
+ const spec = BLIP_TYPES[recType];
98569
+ if (spec) {
98570
+ if (!spec.extractable) return null;
98571
+ if (length2 > payload.length - body) return null;
98572
+ const instance = verInstance >> 4;
98573
+ const skip = spec.skipByInstance[instance];
98574
+ if (skip === void 0 || skip > length2) return null;
98575
+ return { bytes: payload.subarray(body + skip, body + length2), mediaType: spec.mediaType };
98576
+ }
98577
+ if (recType === BSE_RECORD) {
98578
+ if (body + BSE_HEADER > payload.length) return null;
98579
+ offset = body + BSE_HEADER + view.getUint8(body + BSE_CB_NAME);
98580
+ continue;
98581
+ }
98582
+ if ((verInstance & 15) === 15) {
98583
+ offset = body;
98584
+ continue;
98585
+ }
98586
+ if (length2 > payload.length - body) return null;
98587
+ offset = body + length2;
98588
+ }
98589
+ return null;
98590
+ }
98591
+
98497
98592
  // ../ooxml/src/doc/parse.ts
98498
98593
  var FIB_MAGIC2 = 42476;
98499
98594
  var CH = {
98595
+ picture: 1,
98596
+ // inline picture anchor; its CHPX locates the bytes in Data
98500
98597
  refMark: 2,
98501
98598
  // footnote/endnote auto-number, inside a note subdocument
98502
98599
  cellMark: 7,
@@ -98522,6 +98619,8 @@ function parseDocToModel(bytes) {
98522
98619
  if (!tableBytes) throw new DocParseError("Not a legacy .doc: missing Table stream");
98523
98620
  const table = new Reader(tableBytes);
98524
98621
  const diagnostics = [];
98622
+ const dataBytes = findStream(streams, "Data");
98623
+ const pictures = new PictureCollector();
98525
98624
  const clx = readerFor(table, fib.fcLcb(FcLcbIndex.Clx));
98526
98625
  if (!clx) throw new DocParseError("Not a legacy .doc: empty Clx");
98527
98626
  const pieces = PieceTable.parse(wd, clx);
@@ -98546,6 +98645,8 @@ function parseDocToModel(bytes) {
98546
98645
  stsh,
98547
98646
  fonts,
98548
98647
  footnoteRefs: new Map(footnotes.refs.map((r) => [r.cp, r.index])),
98648
+ data: dataBytes ? new Reader(dataBytes) : null,
98649
+ pictures,
98549
98650
  diagnostics
98550
98651
  };
98551
98652
  const bodyParas = parseParagraphs(ctx, 0, fib.ccpText, numbering);
@@ -98568,9 +98669,32 @@ function parseDocToModel(bytes) {
98568
98669
  section: sectionResult.section,
98569
98670
  settings,
98570
98671
  bookmarks,
98672
+ pictures: pictures.list,
98571
98673
  diagnostics
98572
98674
  };
98573
98675
  }
98676
+ var PictureCollector = class {
98677
+ list = [];
98678
+ indexByKey = /* @__PURE__ */ new Map();
98679
+ /** Index of this picture in `list`, adding it on first sight. */
98680
+ intern(picture) {
98681
+ const key = `${picture.mediaType}:${picture.bytes.length}:${fnv1a(picture.bytes)}`;
98682
+ const existing = this.indexByKey.get(key);
98683
+ if (existing !== void 0) return existing;
98684
+ const index = this.list.length;
98685
+ this.list.push({ bytes: picture.bytes, mediaType: picture.mediaType });
98686
+ this.indexByKey.set(key, index);
98687
+ return index;
98688
+ }
98689
+ };
98690
+ function fnv1a(bytes) {
98691
+ let hash2 = 2166136261;
98692
+ for (let i2 = 0; i2 < bytes.length; i2++) {
98693
+ hash2 ^= bytes[i2];
98694
+ hash2 = Math.imul(hash2, 16777619) >>> 0;
98695
+ }
98696
+ return hash2.toString(16);
98697
+ }
98574
98698
  function parseParagraphs(ctx, startCp, endCp, numbering) {
98575
98699
  const paras = [];
98576
98700
  let paraStart = startCp;
@@ -98658,6 +98782,20 @@ function parseParagraphs(ctx, startCp, endCp, numbering) {
98658
98782
  text += " ";
98659
98783
  continue;
98660
98784
  }
98785
+ if (code === CH.picture) {
98786
+ const picture = readPictureAt(ctx, currentRaw, cp);
98787
+ if (picture !== null) {
98788
+ const index = ctx.pictures.intern(picture);
98789
+ pushPart(cp, currentRaw, (chp) => ({
98790
+ kind: "picture",
98791
+ pictureIndex: index,
98792
+ widthEmu: picture.widthEmu,
98793
+ heightEmu: picture.heightEmu,
98794
+ chp
98795
+ }));
98796
+ }
98797
+ continue;
98798
+ }
98661
98799
  if (code < 32) {
98662
98800
  noteControlChar(ctx, cp, code);
98663
98801
  continue;
@@ -98698,9 +98836,22 @@ function finalizeParagraph(ctx, parts, startCp, markCp, isCellMark, numbering) {
98698
98836
  runCps
98699
98837
  };
98700
98838
  }
98839
+ function readPictureAt(ctx, raw, cp) {
98840
+ const note = (detail) => {
98841
+ ctx.diagnostics.push({ kind: "image", location: { cp }, detail });
98842
+ return null;
98843
+ };
98844
+ if (raw.picLocation === void 0) return note("Picture anchor carries no Data-stream location; dropped");
98845
+ if (!ctx.data) return note("Picture anchor found but the document has no Data stream; dropped");
98846
+ const picture = readInlinePicture(ctx.data, raw.picLocation);
98847
+ if (!picture) {
98848
+ return note(`Picture at Data+${raw.picLocation} is not a readable inline image; dropped`);
98849
+ }
98850
+ return picture;
98851
+ }
98701
98852
  function noteControlChar(ctx, cp, code) {
98702
- if (code === 1 || code === 8) {
98703
- ctx.diagnostics.push({ kind: "image", location: { cp }, detail: `Inline object anchor (0x${code.toString(16)}) dropped` });
98853
+ if (code === 8) {
98854
+ ctx.diagnostics.push({ kind: "drawingObject", location: { cp }, detail: "Floating drawing object dropped" });
98704
98855
  } else if (code === 5) {
98705
98856
  ctx.diagnostics.push({ kind: "annotation", location: { cp }, detail: "Annotation reference dropped" });
98706
98857
  }
@@ -100972,8 +101123,66 @@ function textRunContent(text) {
100972
101123
  }
100973
101124
  return out.length > 0 ? out : [textElement("")];
100974
101125
  }
100975
- function specialRunContent(run) {
101126
+ function pictureContent(run, ctx) {
101127
+ const relId = ctx.pictureRelIds[run.pictureIndex];
101128
+ if (relId === void 0) {
101129
+ throw new Error(
101130
+ `Picture ${run.pictureIndex} has no relationship; the emitter must register every picture before building runs`
101131
+ );
101132
+ }
101133
+ const name2 = `Picture ${run.pictureIndex + 1}`;
101134
+ const extent = { "@_cx": num2(run.widthEmu), "@_cy": num2(run.heightEmu) };
101135
+ const blipChildren = [
101136
+ leaf("a:blip", { "@_r:embed": relId }),
101137
+ { "a:stretch": [{ "a:fillRect": [] }] }
101138
+ ];
101139
+ return {
101140
+ "w:drawing": [
101141
+ {
101142
+ "wp:inline": [
101143
+ leaf("wp:extent", extent),
101144
+ leaf("wp:docPr", { "@_id": num2(run.pictureIndex + 1), "@_name": name2 }),
101145
+ {
101146
+ "a:graphic": [
101147
+ {
101148
+ "a:graphicData": [
101149
+ {
101150
+ "pic:pic": [
101151
+ {
101152
+ "pic:nvPicPr": [
101153
+ leaf("pic:cNvPr", { "@_id": num2(run.pictureIndex + 1), "@_name": name2 }),
101154
+ { "pic:cNvPicPr": [] }
101155
+ ]
101156
+ },
101157
+ { "pic:blipFill": blipChildren },
101158
+ {
101159
+ "pic:spPr": [
101160
+ {
101161
+ "a:xfrm": [
101162
+ leaf("a:off", { "@_x": "0", "@_y": "0" }),
101163
+ leaf("a:ext", extent)
101164
+ ]
101165
+ },
101166
+ { "a:prstGeom": [{ "a:avLst": [] }], ":@": { "@_prst": "rect" } }
101167
+ ]
101168
+ }
101169
+ ]
101170
+ }
101171
+ ],
101172
+ ":@": { "@_uri": "http://schemas.openxmlformats.org/drawingml/2006/picture" }
101173
+ }
101174
+ ]
101175
+ }
101176
+ ],
101177
+ ":@": { "@_distT": "0", "@_distB": "0", "@_distL": "0", "@_distR": "0" }
101178
+ }
101179
+ ]
101180
+ };
101181
+ }
101182
+ function specialRunContent(run, ctx) {
100976
101183
  switch (run.kind) {
101184
+ case "picture":
101185
+ return pictureContent(run, ctx);
100977
101186
  case "break":
100978
101187
  return run.breakType === "textWrapping" ? { "w:br": [] } : leaf("w:br", { "@_w:type": run.breakType });
100979
101188
  case "footnoteReference":
@@ -100991,7 +101200,7 @@ function buildRunElement(run, ctx) {
100991
101200
  const rPr = rPrElement(run.chp, ctx);
100992
101201
  if (rPr) children.push(rPr);
100993
101202
  if (run.kind === "text") children.push(...textRunContent(run.text));
100994
- else children.push(specialRunContent(run));
101203
+ else children.push(specialRunContent(run, ctx));
100995
101204
  return { "w:r": children };
100996
101205
  }
100997
101206
  function borderChild(tag, border2) {
@@ -101292,7 +101501,7 @@ function overrideElement(override) {
101292
101501
  const loChildren = [];
101293
101502
  if (lo.startOverride !== void 0) loChildren.push(leaf3("w:startOverride", { "@_w:val": String(lo.startOverride) }));
101294
101503
  if (lo.level) {
101295
- loChildren.push(levelElement(lo.level, { styleIdByIstd: /* @__PURE__ */ new Map() }));
101504
+ loChildren.push(levelElement(lo.level, { styleIdByIstd: /* @__PURE__ */ new Map(), pictureRelIds: [] }));
101296
101505
  }
101297
101506
  children.push({ "w:lvlOverride": loChildren, ":@": { "@_w:ilvl": String(lo.ilvl) } });
101298
101507
  }
@@ -101472,14 +101681,28 @@ function buildFontTableXml(fontNames) {
101472
101681
  const root = { "w:fonts": fonts, ":@": { "@_xmlns:w": W_NS4 } };
101473
101682
  return buildXml([root]);
101474
101683
  }
101684
+ async function registerPictures(doc, pictures) {
101685
+ if (pictures.length === 0) return [];
101686
+ ensureDrawingNamespaces(doc);
101687
+ const relIds = [];
101688
+ for (let i2 = 0; i2 < pictures.length; i2++) {
101689
+ const extension = pictureExtension(pictures[i2].mediaType);
101690
+ const name2 = `image${i2 + 1}.${extension}`;
101691
+ doc.zip.file(`word/media/${name2}`, pictures[i2].bytes);
101692
+ await ensureContentType(doc, extension, pictures[i2].mediaType);
101693
+ relIds.push(await addRelationship(doc, `${REL}image`, `media/${name2}`));
101694
+ }
101695
+ return relIds;
101696
+ }
101475
101697
  async function docModelToDocx(model) {
101476
101698
  const styleIdByIstd = /* @__PURE__ */ new Map();
101477
101699
  for (const style of model.styles) styleIdByIstd.set(style.istd, style.styleId);
101478
101700
  if (!styleIdByIstd.has(0)) styleIdByIstd.set(0, "Normal");
101479
- const ctx = { styleIdByIstd };
101480
101701
  const walked = walkModel(model);
101481
101702
  const hasFootnotes = model.footnotes.length > 0;
101482
101703
  const doc = createBlankDocx();
101704
+ const pictureRelIds = await registerPictures(doc, model.pictures);
101705
+ const ctx = { styleIdByIstd, pictureRelIds };
101483
101706
  doc.bodyElements = [...buildBody(model, ctx), buildSectPr(model.section)];
101484
101707
  doc.zip.file("word/styles.xml", buildStylesXml2(model, ctx, walked.referencedStyleIstds));
101485
101708
  doc.zip.file("word/settings.xml", buildSettingsXml(model, hasFootnotes));
@@ -101720,6 +101943,18 @@ async function addRelationship(doc, type, target, targetMode) {
101720
101943
  doc.zip.file("word/_rels/document.xml.rels", updated);
101721
101944
  return rId;
101722
101945
  }
101946
+ async function ensureContentType(doc, extension, contentType) {
101947
+ const contentTypesFile = doc.zip.file("[Content_Types].xml");
101948
+ if (!contentTypesFile) return;
101949
+ const ct = await contentTypesFile.async("string");
101950
+ if (ct.includes(`Extension="${extension}"`)) return;
101951
+ const updated = ct.replace(
101952
+ "</Types>",
101953
+ ` <Default Extension="${extension}" ContentType="${contentType}"/>
101954
+ </Types>`
101955
+ );
101956
+ doc.zip.file("[Content_Types].xml", updated);
101957
+ }
101723
101958
  async function ensureContentTypeOverride(doc, partName, contentType) {
101724
101959
  const contentTypesFile = doc.zip.file("[Content_Types].xml");
101725
101960
  if (!contentTypesFile) return;
@@ -101732,6 +101967,17 @@ async function ensureContentTypeOverride(doc, partName, contentType) {
101732
101967
  );
101733
101968
  doc.zip.file("[Content_Types].xml", updated);
101734
101969
  }
101970
+ function ensureDrawingNamespaces(doc) {
101971
+ if (!doc.documentAttrs["@_xmlns:a"]) {
101972
+ doc.documentAttrs["@_xmlns:a"] = "http://schemas.openxmlformats.org/drawingml/2006/main";
101973
+ }
101974
+ if (!doc.documentAttrs["@_xmlns:pic"]) {
101975
+ doc.documentAttrs["@_xmlns:pic"] = "http://schemas.openxmlformats.org/drawingml/2006/picture";
101976
+ }
101977
+ if (!doc.documentAttrs["@_xmlns:wp"]) {
101978
+ doc.documentAttrs["@_xmlns:wp"] = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
101979
+ }
101980
+ }
101735
101981
 
101736
101982
  // ../docx/src/parse/parser.ts
101737
101983
  var import_jszip3 = __toESM(require_lib4(), 1);
@@ -2,6 +2,31 @@ import { transportErrorMessage } from "@lotics/shared/transport_error";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { getInvocation } from "./invocation.js";
5
+ /**
6
+ * The filename a `Content-Disposition` names, preferring RFC 5987 `filename*`.
7
+ *
8
+ * The server always sends BOTH: `filename*=UTF-8''…` carrying the real name,
9
+ * and a plain `filename=` whose non-ASCII characters are replaced with `_`
10
+ * because HTTP header values are Latin-1. Reading only the plain one turned
11
+ * every accented name into underscores on the way to disk — `GIẤY YÊU CẦU.doc`
12
+ * saved as `GI_Y Y_U C_U.doc` — even though the correct bytes were in the same
13
+ * header. The `*` form wins whenever it parses; the plain form is the fallback
14
+ * it was always meant to be.
15
+ */
16
+ function parseContentDispositionFilename(disposition) {
17
+ const extended = disposition.match(/filename\*=\s*([^']*)'[^']*'([^;\n]+)/i);
18
+ if (extended) {
19
+ try {
20
+ return decodeURIComponent(extended[2].trim());
21
+ }
22
+ catch {
23
+ // A malformed percent-escape means this parameter is unusable, not that
24
+ // the whole header is — fall through to the ASCII form below.
25
+ }
26
+ }
27
+ const plain = disposition.match(/filename=\s*"?([^";\n]+)"?/i);
28
+ return plain ? plain[1].trim() : null;
29
+ }
5
30
  function findAvailableFilename(dir, filename, reserved) {
6
31
  // `reserved` tracks absolute paths claimed by in-flight downloads in the same
7
32
  // batch — required for parallel callers because the file may not be on disk
@@ -824,8 +849,7 @@ export class LoticsClient {
824
849
  if (!response.ok)
825
850
  await this.throwResponseError(response);
826
851
  const disposition = response.headers.get("content-disposition") ?? "";
827
- const match = disposition.match(/filename="?([^";\n]+)"?/);
828
- const originalFilename = match?.[1] ?? fileId;
852
+ const originalFilename = parseContentDispositionFilename(disposition) ?? fileId;
829
853
  const buffer = Buffer.from(await response.arrayBuffer());
830
854
  const dir = outputDir ? path.resolve(outputDir) : process.cwd();
831
855
  // The bytes are already fetched by this point, so a missing output directory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.152.1",
3
+ "version": "0.152.2",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {