@lotics/cli 0.96.4 → 0.96.6

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.
Files changed (2) hide show
  1. package/dist/src/cli.js +284 -13
  2. package/package.json +1 -1
package/dist/src/cli.js CHANGED
@@ -70166,6 +70166,16 @@ function runNpm(args, cwd) {
70166
70166
  });
70167
70167
  });
70168
70168
  }
70169
+ function readAppWorkspaceId(projectDir) {
70170
+ try {
70171
+ const pkgPath2 = path5.join(projectDir, "package.json");
70172
+ if (!fs4.existsSync(pkgPath2)) return null;
70173
+ const pkg2 = JSON.parse(fs4.readFileSync(pkgPath2, "utf-8"));
70174
+ return pkg2.lotics?.workspace_id ?? null;
70175
+ } catch {
70176
+ return null;
70177
+ }
70178
+ }
70169
70179
  function readAppMeta(projectDir) {
70170
70180
  const pkgPath2 = path5.join(projectDir, "package.json");
70171
70181
  if (!fs4.existsSync(pkgPath2)) {
@@ -90242,6 +90252,170 @@ async function ensureContentTypeOverride(doc, partName, contentType) {
90242
90252
  doc.zip.file("[Content_Types].xml", updated);
90243
90253
  }
90244
90254
 
90255
+ // ../ooxml/src/queries.ts
90256
+ function getBodyElementType(el) {
90257
+ const tag = getTagName(el);
90258
+ if (tag === "w:p") return "paragraph";
90259
+ if (tag === "w:tbl") return "table";
90260
+ return "other";
90261
+ }
90262
+ function extractParagraphText(el) {
90263
+ const tag = getTagName(el);
90264
+ if (tag !== "w:p") return "";
90265
+ const parts = [];
90266
+ for (const child of getChildren(el)) {
90267
+ const childTag = getTagName(child);
90268
+ if (childTag === "w:r") {
90269
+ for (const runChild of getChildren(child)) {
90270
+ if (getTagName(runChild) === "w:t") {
90271
+ parts.push(getTextContent(runChild));
90272
+ }
90273
+ }
90274
+ } else if (childTag === "w:hyperlink") {
90275
+ for (const hlChild of getChildren(child)) {
90276
+ if (getTagName(hlChild) === "w:r") {
90277
+ for (const runChild of getChildren(hlChild)) {
90278
+ if (getTagName(runChild) === "w:t") {
90279
+ parts.push(getTextContent(runChild));
90280
+ }
90281
+ }
90282
+ }
90283
+ }
90284
+ }
90285
+ }
90286
+ return parts.join("");
90287
+ }
90288
+ function getRunsFromParagraph(el) {
90289
+ const runs = [];
90290
+ for (const child of getChildren(el)) {
90291
+ if (getTagName(child) !== "w:r") continue;
90292
+ let text = "";
90293
+ let textIdx = -1;
90294
+ const children = getChildren(child);
90295
+ for (let i2 = 0; i2 < children.length; i2++) {
90296
+ if (getTagName(children[i2]) === "w:t") {
90297
+ const tChildren = getChildren(children[i2]);
90298
+ for (const tn of tChildren) {
90299
+ if (typeof tn["#text"] === "string") {
90300
+ text += tn["#text"];
90301
+ }
90302
+ }
90303
+ textIdx = i2;
90304
+ }
90305
+ }
90306
+ if (textIdx >= 0) {
90307
+ runs.push({ element: child, text, textNodeIndex: textIdx });
90308
+ }
90309
+ }
90310
+ return runs;
90311
+ }
90312
+ function setRunText(run, newText) {
90313
+ const children = getChildren(run.element);
90314
+ children[run.textNodeIndex] = {
90315
+ "w:t": [{ "#text": newText }],
90316
+ ":@": { "@_xml:space": "preserve" }
90317
+ };
90318
+ run.text = newText;
90319
+ }
90320
+ function computeReplacementRanges(text, query, replacement, matchType, maxReplacements, startCount) {
90321
+ const ranges = [];
90322
+ let count = startCount;
90323
+ if (matchType === "exact") {
90324
+ if (text === query && count < maxReplacements) {
90325
+ ranges.push({ start: 0, end: text.length, replacement });
90326
+ count++;
90327
+ }
90328
+ return { ranges, count };
90329
+ }
90330
+ if (matchType === "regex") {
90331
+ const re2 = new RegExp(query, "gi");
90332
+ let match2;
90333
+ while (count < maxReplacements && (match2 = re2.exec(text)) !== null) {
90334
+ ranges.push({ start: match2.index, end: match2.index + match2[0].length, replacement });
90335
+ count++;
90336
+ if (match2[0].length === 0) re2.lastIndex++;
90337
+ }
90338
+ return { ranges, count };
90339
+ }
90340
+ if (query.length === 0) return { ranges, count };
90341
+ const lower2 = text.toLowerCase();
90342
+ const lowerQuery = query.toLowerCase();
90343
+ let searchFrom = 0;
90344
+ while (count < maxReplacements) {
90345
+ const idx = lower2.indexOf(lowerQuery, searchFrom);
90346
+ if (idx === -1) break;
90347
+ ranges.push({ start: idx, end: idx + query.length, replacement });
90348
+ searchFrom = idx + query.length;
90349
+ count++;
90350
+ }
90351
+ return { ranges, count };
90352
+ }
90353
+ function applyRangeReplacements(runs, ranges) {
90354
+ if (ranges.length === 0) return;
90355
+ const bounds = [];
90356
+ let offset = 0;
90357
+ for (const run of runs) {
90358
+ bounds.push({ run, start: offset, end: offset + run.text.length });
90359
+ offset += run.text.length;
90360
+ }
90361
+ const joined = runs.map((r) => r.text).join("");
90362
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
90363
+ for (const { run, start: runStart, end: runEnd } of bounds) {
90364
+ let next = "";
90365
+ let cursor = runStart;
90366
+ for (const range2 of sorted) {
90367
+ if (range2.end <= runStart || range2.start >= runEnd) continue;
90368
+ const keepUntil = Math.min(range2.start, runEnd);
90369
+ const from = Math.max(cursor, runStart);
90370
+ if (keepUntil > from) next += joined.slice(from, keepUntil);
90371
+ if (runStart <= range2.start && range2.start < runEnd) next += range2.replacement;
90372
+ cursor = Math.max(cursor, range2.end);
90373
+ }
90374
+ const tail = Math.max(cursor, runStart);
90375
+ if (runEnd > tail) next += joined.slice(tail, runEnd);
90376
+ if (next !== run.text) setRunText(run, next);
90377
+ }
90378
+ }
90379
+ function replaceInParagraph(el, query, replacement, matchType, maxReplacements, replacementsMade) {
90380
+ if (!extractParagraphText(el)) return replacementsMade;
90381
+ const runs = getRunsFromParagraph(el);
90382
+ if (runs.length === 0) return replacementsMade;
90383
+ const text = runs.length === 1 ? runs[0].text : runs.map((r) => r.text).join("");
90384
+ const { ranges, count } = computeReplacementRanges(text, query, replacement, matchType, maxReplacements, replacementsMade);
90385
+ applyRangeReplacements(runs, ranges);
90386
+ return count;
90387
+ }
90388
+ function replaceText(bodyElements, query, replacement, matchType = "contains", maxReplacements = Infinity) {
90389
+ if (matchType === "regex") {
90390
+ try {
90391
+ new RegExp(query);
90392
+ } catch {
90393
+ throw new Error(`Invalid regex pattern: ${query}`);
90394
+ }
90395
+ }
90396
+ let replacementsMade = 0;
90397
+ for (let i2 = 0; i2 < bodyElements.length && replacementsMade < maxReplacements; i2++) {
90398
+ const el = bodyElements[i2];
90399
+ const type = getBodyElementType(el);
90400
+ if (type === "paragraph") {
90401
+ replacementsMade = replaceInParagraph(el, query, replacement, matchType, maxReplacements, replacementsMade);
90402
+ } else if (type === "table") {
90403
+ for (const child of getChildren(el)) {
90404
+ if (getTagName(child) !== "w:tr") continue;
90405
+ for (const cell of getChildren(child)) {
90406
+ if (getTagName(cell) !== "w:tc") continue;
90407
+ for (const p of getChildren(cell)) {
90408
+ if (getTagName(p) === "w:p" && replacementsMade < maxReplacements) {
90409
+ replacementsMade = replaceInParagraph(p, query, replacement, matchType, maxReplacements, replacementsMade);
90410
+ }
90411
+ }
90412
+ }
90413
+ }
90414
+ }
90415
+ }
90416
+ return replacementsMade;
90417
+ }
90418
+
90245
90419
  // ../docx/src/parse/parser.ts
90246
90420
  var import_jszip2 = __toESM(require_lib4(), 1);
90247
90421
  async function parseDocx(buffer) {
@@ -90768,18 +90942,102 @@ async function docxReplaceText(filePath, rest) {
90768
90942
  fail2("Usage: lotics docx replace-text <file> '<search>' '<replace>'");
90769
90943
  }
90770
90944
  const doc = await loadFile2(filePath);
90771
- const newChildren = doc.body.children.map((b) => b.kind === "paragraph" ? replaceInParagraph(b, search, replace2) : b);
90772
- await writeDoc(filePath, replaceChildren(doc, newChildren));
90945
+ const { children, count } = replaceEverywhere(doc.body.children, search, replace2);
90946
+ if (count === 0) {
90947
+ fail2(`No occurrence of ${JSON.stringify(search)} in ${filePath} \u2014 nothing written.`);
90948
+ }
90949
+ await writeDoc(filePath, replaceChildren(doc, children));
90950
+ console.error(`Wrote ${filePath} (${count} replacement${count === 1 ? "" : "s"})`);
90951
+ }
90952
+ function replaceEverywhere(blocks, search, replace2) {
90953
+ let count = 0;
90954
+ const children = blocks.map((b) => {
90955
+ if (b.kind === "paragraph") {
90956
+ const [paragraph, n] = replaceInParagraph2(b, search, replace2);
90957
+ count += n;
90958
+ return paragraph;
90959
+ }
90960
+ if (b.kind === "opaque_block") {
90961
+ const xml = structuredClone(b.xml);
90962
+ const n = replaceText([xml], search, replace2, "contains");
90963
+ if (n === 0) return b;
90964
+ count += n;
90965
+ return { kind: "opaque_block", xml };
90966
+ }
90967
+ return b;
90968
+ });
90969
+ return { children, count };
90773
90970
  }
90774
- function replaceInParagraph(p, search, replace2) {
90775
- const content = p.content.map((inline) => {
90971
+ function replaceInParagraph2(p, search, replace2) {
90972
+ if (search === "") return [p, 0];
90973
+ const rewritten = /* @__PURE__ */ new Map();
90974
+ let count = 0;
90975
+ for (const segment of collectTextSegments(p)) {
90976
+ const joined = segment.map((s) => s.value).join("");
90977
+ const matches = [];
90978
+ for (let at2 = joined.indexOf(search); at2 !== -1; at2 = joined.indexOf(search, at2 + search.length)) {
90979
+ matches.push({ start: at2, end: at2 + search.length });
90980
+ }
90981
+ if (matches.length === 0) continue;
90982
+ count += matches.length;
90983
+ for (const slot of segment) {
90984
+ let out = "";
90985
+ let cursor = slot.start;
90986
+ for (const m of matches) {
90987
+ if (m.end <= slot.start || m.start >= slot.end) continue;
90988
+ const keepUntil = Math.max(cursor, Math.min(m.start, slot.end));
90989
+ if (keepUntil > cursor) out += joined.slice(cursor, keepUntil);
90990
+ if (m.start >= slot.start && m.start < slot.end) out += replace2;
90991
+ cursor = Math.max(cursor, Math.min(m.end, slot.end));
90992
+ }
90993
+ if (cursor < slot.end) out += joined.slice(cursor, slot.end);
90994
+ rewritten.set(`${slot.inlineIndex}:${slot.childIndex}`, out);
90995
+ }
90996
+ }
90997
+ if (count === 0) return [p, 0];
90998
+ const content = p.content.map((inline, inlineIndex) => {
90776
90999
  if (inline.kind !== "run") return inline;
90777
- const newChildren = inline.content.map(
90778
- (c) => c.kind === "text" ? { kind: "text", value: c.value.split(search).join(replace2), preserveSpace: c.preserveSpace } : c
90779
- );
90780
- return { kind: "run", properties: inline.properties, content: newChildren };
91000
+ const children = inline.content.map((c, childIndex) => {
91001
+ if (c.kind !== "text") return c;
91002
+ const next = rewritten.get(`${inlineIndex}:${childIndex}`);
91003
+ if (next === void 0 || next === c.value) return c;
91004
+ return { kind: "text", value: next, preserveSpace: next.startsWith(" ") || next.endsWith(" ") };
91005
+ });
91006
+ return { kind: "run", properties: inline.properties, content: children };
90781
91007
  });
90782
- return { kind: "paragraph", properties: p.properties, content };
91008
+ return [{ kind: "paragraph", properties: p.properties, content }, count];
91009
+ }
91010
+ function collectTextSegments(p) {
91011
+ const segments = [];
91012
+ let current = [];
91013
+ let offset = 0;
91014
+ const breakSegment = () => {
91015
+ if (current.length > 0) segments.push(current);
91016
+ current = [];
91017
+ offset = 0;
91018
+ };
91019
+ p.content.forEach((inline, inlineIndex) => {
91020
+ if (inline.kind !== "run") {
91021
+ breakSegment();
91022
+ return;
91023
+ }
91024
+ inline.content.forEach((child, childIndex) => {
91025
+ if (child.kind !== "text") {
91026
+ breakSegment();
91027
+ return;
91028
+ }
91029
+ current.push({
91030
+ inlineIndex,
91031
+ childIndex,
91032
+ value: child.value,
91033
+ start: offset,
91034
+ end: offset + child.value.length
91035
+ });
91036
+ offset += child.value.length;
91037
+ });
91038
+ });
91039
+ breakSegment();
91040
+ return segments.filter((s) => s.length > 0);
90783
91041
  }
90784
91042
  async function docxBatch(filePath, json2) {
90785
91043
  if (!filePath || !json2) fail2("Usage: lotics docx batch <file> '<json-array-of-ops>'");
@@ -90823,8 +91081,9 @@ function applyBatchOp(doc, op, index) {
90823
91081
  return replaceChildren(doc, doc.body.children.filter((_, i2) => i2 !== op.at));
90824
91082
  }
90825
91083
  case "replace-text": {
90826
- const next = doc.body.children.map((b) => b.kind === "paragraph" ? replaceInParagraph(b, op.search, op.replace) : b);
90827
- return replaceChildren(doc, next);
91084
+ const { children, count } = replaceEverywhere(doc.body.children, op.search, op.replace);
91085
+ if (count === 0) fail2(`[op ${index}] No occurrence of ${JSON.stringify(op.search)} \u2014 batch aborted.`);
91086
+ return replaceChildren(doc, children);
90828
91087
  }
90829
91088
  default: {
90830
91089
  const exhaustive = op;
@@ -90844,10 +91103,12 @@ Uses Lotics' own OOXML engine; round-trips faithfully with the Lotics editor and
90844
91103
  lotics docx insert-paragraph <file> '<text>' --at=<i> [--style=NAME]
90845
91104
  Insert a paragraph at index <i> (0-based)
90846
91105
  lotics docx delete-block <file> --at=<i> Remove the block at index <i>
90847
- lotics docx replace-text <file> '<search>' '<replace>' Replace text in all paragraph runs
91106
+ lotics docx replace-text <file> '<search>' '<replace>' Replace text in paragraphs AND table cells
91107
+ (errors if nothing matched \u2014 never a silent no-op)
90848
91108
  lotics docx batch <file> '<json-array-of-ops>' Apply many ops in one parse/serialize cycle
90849
91109
 
90850
- Edit ops mutate the file atomically (temp file + rename). Opaque blocks (tables, custom XML) are preserved verbatim.`);
91110
+ Edit ops mutate the file atomically (temp file + rename). Structure inside opaque blocks (tables, custom XML) is
91111
+ preserved verbatim; replace-text rewrites the text within them.`);
90851
91112
  }
90852
91113
  async function runDocxCommand(subcommand, toolArgs, restArgs) {
90853
91114
  switch (subcommand) {
@@ -91542,6 +91803,14 @@ function announceTarget(ctx, workspaceId) {
91542
91803
  }
91543
91804
  console.error(`lotics \u2192 ${org} / ${workspaceId}`);
91544
91805
  }
91806
+ function applyAppManifestWorkspace(ctx, command, subcommand, pathArg, flags) {
91807
+ if (command !== "app") return;
91808
+ if (flags.workspace || process.env.LOTICS_WORKSPACE) return;
91809
+ if (subcommand === "create" || subcommand === "pull") return;
91810
+ const dir = pathArg && !pathArg.startsWith("-") ? pathArg : process.cwd();
91811
+ const workspaceId = readAppWorkspaceId(dir);
91812
+ if (workspaceId) ctx.workspaceId = workspaceId;
91813
+ }
91545
91814
  async function resolveWorkspace(client, ctx) {
91546
91815
  if (ctx.workspaceId) {
91547
91816
  announceTarget(ctx, ctx.workspaceId);
@@ -91744,6 +92013,7 @@ async function main() {
91744
92013
  await appCodegen({ projectDir });
91745
92014
  return;
91746
92015
  }
92016
+ applyAppManifestWorkspace(ctx2, command, subcommand, projectDir, flags);
91747
92017
  const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
91748
92018
  const client2 = new LoticsClient({ apiKey: ctx2.apiKey, workspaceId: ctx2.workspaceId, viewAsMemberId });
91749
92019
  await resolveWorkspace(client2, ctx2);
@@ -91969,6 +92239,7 @@ Available workspaces:`);
91969
92239
  if (ctx.orgName) console.error(`Org: ${ctx.orgName}`);
91970
92240
  return;
91971
92241
  }
92242
+ applyAppManifestWorkspace(ctx, command, subcommand, subcommand === "dev" ? toolArgs : void 0, flags);
91972
92243
  await resolveWorkspace(client, ctx);
91973
92244
  if (command === "knowledge") {
91974
92245
  await runKnowledgeCommand(client, subcommand, toolArgs, flags);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.96.4",
3
+ "version": "0.96.6",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {