@kodocagent/cli 0.4.4 → 0.5.0

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/index.js CHANGED
@@ -9739,9 +9739,9 @@ var require_load = __commonJS({
9739
9739
  var require_lib3 = __commonJS({
9740
9740
  "../../node_modules/.pnpm/jszip@3.10.1/node_modules/jszip/lib/index.js"(exports, module) {
9741
9741
  "use strict";
9742
- function JSZip5() {
9743
- if (!(this instanceof JSZip5)) {
9744
- return new JSZip5();
9742
+ function JSZip7() {
9743
+ if (!(this instanceof JSZip7)) {
9744
+ return new JSZip7();
9745
9745
  }
9746
9746
  if (arguments.length) {
9747
9747
  throw new Error("The constructor with parameters has been removed in JSZip 3.0, please check the upgrade guide.");
@@ -9750,7 +9750,7 @@ var require_lib3 = __commonJS({
9750
9750
  this.comment = null;
9751
9751
  this.root = "";
9752
9752
  this.clone = function() {
9753
- var newObj = new JSZip5();
9753
+ var newObj = new JSZip7();
9754
9754
  for (var i in this) {
9755
9755
  if (typeof this[i] !== "function") {
9756
9756
  newObj[i] = this[i];
@@ -9759,16 +9759,16 @@ var require_lib3 = __commonJS({
9759
9759
  return newObj;
9760
9760
  };
9761
9761
  }
9762
- JSZip5.prototype = require_object();
9763
- JSZip5.prototype.loadAsync = require_load();
9764
- JSZip5.support = require_support();
9765
- JSZip5.defaults = require_defaults();
9766
- JSZip5.version = "3.10.1";
9767
- JSZip5.loadAsync = function(content, options) {
9768
- return new JSZip5().loadAsync(content, options);
9762
+ JSZip7.prototype = require_object();
9763
+ JSZip7.prototype.loadAsync = require_load();
9764
+ JSZip7.support = require_support();
9765
+ JSZip7.defaults = require_defaults();
9766
+ JSZip7.version = "3.10.1";
9767
+ JSZip7.loadAsync = function(content, options) {
9768
+ return new JSZip7().loadAsync(content, options);
9769
9769
  };
9770
- JSZip5.external = require_external();
9771
- module.exports = JSZip5;
9770
+ JSZip7.external = require_external();
9771
+ module.exports = JSZip7;
9772
9772
  }
9773
9773
  });
9774
9774
 
@@ -9851,6 +9851,58 @@ var KODOC_PATHS = {
9851
9851
  function projectMcpConfigPath(cwd) {
9852
9852
  return join(cwd, ".kodocagent", "mcp.json");
9853
9853
  }
9854
+ var PATTERNS = [
9855
+ {
9856
+ type: "\uC8FC\uBBFC\uB4F1\uB85D\uBC88\uD638",
9857
+ re: /\b\d{6}-[1-4]\d{6}\b/g,
9858
+ mask: (m) => `${m.slice(0, 8)}******`
9859
+ },
9860
+ {
9861
+ type: "\uC2E0\uC6A9\uCE74\uB4DC\uBC88\uD638",
9862
+ re: /\b\d{4}-\d{4}-\d{4}-\d{4}\b/g,
9863
+ mask: (m) => `${m.slice(0, 4)}-****-****-${m.slice(-4)}`
9864
+ },
9865
+ {
9866
+ type: "\uC804\uD654\uBC88\uD638",
9867
+ re: /\b0\d{1,2}-\d{3,4}-\d{4}\b/g,
9868
+ mask: (m) => {
9869
+ const p = m.split("-");
9870
+ return `${p[0]}-${"*".repeat((p[1] ?? "").length)}-${p[2]}`;
9871
+ }
9872
+ },
9873
+ {
9874
+ type: "\uC774\uBA54\uC77C",
9875
+ re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
9876
+ mask: (m) => {
9877
+ const [u, d] = m.split("@");
9878
+ return `${u?.[0] ?? ""}***@${d}`;
9879
+ }
9880
+ }
9881
+ ];
9882
+ function detectPii(text3) {
9883
+ if (!text3) return [];
9884
+ const out = [];
9885
+ for (const { type, re, mask } of PATTERNS) {
9886
+ const matches = text3.match(re);
9887
+ if (matches && matches.length > 0) {
9888
+ const uniq = [...new Set(matches)];
9889
+ out.push({ type, count: matches.length, masked: uniq.slice(0, 5).map(mask) });
9890
+ }
9891
+ }
9892
+ return out;
9893
+ }
9894
+ function summarizePii(findings) {
9895
+ return findings.map((f) => `${f.type} ${f.count}\uAC74`).join(", ");
9896
+ }
9897
+ function redactText(text3) {
9898
+ if (!text3) return { text: text3 ?? "", findings: [] };
9899
+ let result = text3;
9900
+ const findings = detectPii(text3);
9901
+ for (const { re, mask } of PATTERNS) {
9902
+ result = result.replace(new RegExp(re.source, re.flags), (m) => mask(m));
9903
+ }
9904
+ return { text: result, findings };
9905
+ }
9854
9906
 
9855
9907
  // ../core/dist/index.js
9856
9908
  import { stepCountIs, streamText } from "ai";
@@ -9869,7 +9921,12 @@ import { appendFile, mkdir as mkdir2, readdir, readFile as readFile2, stat } fro
9869
9921
  import { join as join2 } from "path";
9870
9922
  import { tool } from "ai";
9871
9923
  function buildSystemPrompt(ctx) {
9872
- const stable = [ROLE_SECTION, DOCUMENT_RULES_SECTION, LAW_RULES_SECTION].join("\n\n");
9924
+ const stable = [
9925
+ ROLE_SECTION,
9926
+ DOCUMENT_RULES_SECTION,
9927
+ EDIT_SAFETY_SECTION,
9928
+ LAW_RULES_SECTION
9929
+ ].join("\n\n");
9873
9930
  const dynamic = buildDynamicContext(ctx);
9874
9931
  return `${stable}
9875
9932
 
@@ -9892,6 +9949,15 @@ var DOCUMENT_RULES_SECTION = `## \uBB38\uC11C \uADDC\uCE59
9892
9949
  6. \uC0AC\uC6A9\uC790\uAC00 \uC218\uC815\uC548\uC744 \uAC70\uC808\uD558\uBA74 \uAC19\uC740 \uC81C\uC548\uC744 \uC790\uB3D9\uC73C\uB85C \uBC18\uBCF5\uD558\uC9C0 \uB9D0\uACE0, \uC0AC\uC6A9\uC790\uC758 \uB2E4\uC74C \uC9C0\uC2DC\uB97C \uAE30\uB2E4\uB9AC\uC138\uC694.
9893
9950
  7. \uD070 \uBB38\uC11C\uB294 \uBA3C\uC800 \`outline\`\uC73C\uB85C \uAD6C\uC870\uB97C \uD30C\uC545\uD558\uACE0, \`search\`\uB098 \`pages\`\uB85C \uD544\uC694\uD55C \uBD80\uBD84\uB9CC \uC77D\uC5B4 \uCEE8\uD14D\uC2A4\uD2B8\uB97C \uC544\uB07C\uC138\uC694.
9894
9951
  8. \uC0AC\uC6A9\uC790\uAC00 \uC9C1\uC804 \uBCC0\uACBD\uC744 \uB418\uB3CC\uB9AC\uAE38 \uC6D0\uD558\uBA74 \`list_backups\`\uB85C \uBC31\uC5C5\uC744 \uD655\uC778\uD558\uACE0 \`restore_backup\`\uC73C\uB85C \uBCF5\uC6D0\uD558\uC138\uC694. \uBCF5\uC6D0\uB3C4 \uC2B9\uC778\uC744 \uAC70\uCE58\uBA70, \uBCF5\uC6D0 \uC804 \uD604\uC7AC \uC0C1\uD0DC\uAC00 \uC790\uB3D9 \uBC31\uC5C5\uB429\uB2C8\uB2E4.`;
9952
+ var EDIT_SAFETY_SECTION = `## \uD3B8\uC9D1 \uC548\uC804 \uADDC\uCE59
9953
+
9954
+ 1. \uC694\uCCAD\uBC1B\uC740 \uBD80\uBD84\uB9CC \uC218\uC815\uD558\uACE0, \uC694\uCCAD\uD558\uC9C0 \uC54A\uC740 \uBB38\uC7A5\xB7\uC11C\uC2DD\xB7\uAD6C\uC870\xB7\uD45C\uD604\uC740 \uADF8\uB300\uB85C \uB461\uB2C8\uB2E4. \uBB38\uC11C \uC804\uCCB4\uB97C \uC784\uC758\uB85C \uC7AC\uC791\uC131\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
9955
+ 2. \uBB38\uC11C\uC5D0 \uC5C6\uB294 \uC815\uBCF4\uB97C \uB9CC\uB4E4\uC5B4\uB0B4\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC218\uCE58\xB7\uAE08\uC561\xB7\uB0A0\uC9DC\xB7\uC778\uBA85\xB7\uAE30\uAD00\uBA85\xB7\uACC4\uC57D \uC870\uD56D \uB4F1\uC744 \uCD94\uCE21\uD574 \uC0C8\uB85C \uC4F0\uAC70\uB098 \uBC14\uAFB8\uC9C0 \uC54A\uC73C\uBA70, \uBD88\uD655\uC2E4\uD558\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uD655\uC778\uD569\uB2C8\uB2E4.
9956
+ 3. \uC0AC\uC6A9\uC790\uAC00 \uBA85\uC2DC\uC801\uC73C\uB85C \uBC14\uAFB8\uB77C\uACE0 \uD558\uC9C0 \uC54A\uC740 \uD55C \uC22B\uC790\xB7\uAE08\uC561\xB7\uB0A0\uC9DC\xB7\uB2E8\uC704\xB7\uACE0\uC720\uBA85\uC0AC\xB7\uBC95\uB839 \uC870\uBB38 \uBC88\uD638\uC640 \uC778\uC6A9\uBD80\uD638(" ", ' ', \u300C \u300D) \uB0B4\uBD80 \uB0B4\uC6A9\uC740 \uBCF4\uC874\uD569\uB2C8\uB2E4.
9957
+ 4. \uC804\uBB38 \uC6A9\uC5B4\uB294 \uC784\uC758\uB85C \uB2E4\uB978 \uD45C\uD604\uC73C\uB85C \uBC14\uAFB8\uC9C0 \uC54A\uACE0 \uBB38\uC11C \uC804\uCCB4\uC5D0\uC11C \uC77C\uAD00\uB418\uAC8C \uC720\uC9C0\uD569\uB2C8\uB2E4.
9958
+ 5. \uACC4\uC57D\uC11C\xB7\uC57D\uAD00\xB7\uACF5\uC2DC\xB7\uB17C\uBB38 \uB4F1 \uBC95\uC801\xB7\uACF5\uC2DD \uBB38\uC11C\uB294 \uB2E8\uC5B4 \uD558\uB098\uAC00 \uC758\uBBF8\uB97C \uBC14\uAFC0 \uC218 \uC788\uC73C\uBBC0\uB85C, \uD45C\uD604\uC744 \uC790\uB3D9\uC73C\uB85C \uB2E4\uB4EC\uC9C0 \uB9D0\uACE0 \uC0AC\uC6A9\uC790\uAC00 \uC9C0\uC815\uD55C \uBCC0\uACBD\uB9CC \uC218\uD589\uD569\uB2C8\uB2E4.
9959
+ 6. \uC758\uBBF8\uAC00 \uBC14\uB014 \uC218 \uC788\uB294 \uC218\uC815\uC740 \uC2E4\uD589 \uC804\uC5D0 \uADF8 \uC0AC\uC2E4\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC54C\uB9AC\uACE0, \uC218\uC815 \uC81C\uC548\uC758 \uC694\uC57D(summary)\uC5D0\uB294 \uBB34\uC5C7\uC744\xB7\uC65C \uBC14\uAFB8\uB294\uC9C0 \uD568\uAED8 \uC801\uC2B5\uB2C8\uB2E4.
9960
+ 7. \uAC1C\uC778\uC815\uBCF4(\uC8FC\uBBFC\uB4F1\uB85D\uBC88\uD638\xB7\uC804\uD654\uBC88\uD638\xB7\uC774\uBA54\uC77C\xB7\uACC4\uC88C\xB7\uCE74\uB4DC\uBC88\uD638 \uB4F1) \uCC98\uB9AC: \uC0AC\uC6A9\uC790\uAC00 **\uD655\uC778\xB7\uC810\uAC80\uB9CC** \uC6D0\uD558\uBA74 \`scan_pii\`(\uC77D\uAE30 \uC804\uC6A9)\uB85C \uBCF4\uC5EC\uC8FC\uACE0, **\uAC00\uB9AC\uAE30\xB7\uB9C8\uC2A4\uD0B9\xB7\uBE44\uC2DD\uBCC4\xB7\uAC1C\uC778\uC815\uBCF4 \uC0AD\uC81C**\uB97C \uC6D0\uD558\uBA74 \`scan_pii\`\uC5D0\uC11C \uBA48\uCD94\uC9C0 \uB9D0\uACE0 \`propose_redact_pii\`\uB85C \uBC14\uB85C \uC218\uC815\uC548\uC744 \uC81C\uC548\uD558\uC138\uC694(\uC774 \uB3C4\uAD6C\uB3C4 \uC2B9\uC778\uC744 \uAC70\uCE58\uBBC0\uB85C \uC548\uC804\uD569\uB2C8\uB2E4).`;
9895
9961
  var LAW_RULES_SECTION = `## \uBC95\uB839 \uADDC\uCE59
9896
9962
 
9897
9963
  1. \uBC95\uB839 \uC778\uC6A9 \uD615\uC2DD: \u300C\uBC95\uB839\uBA85\u300D \uC81CN\uC870 \uC81CN\uD56D \uC81CN\uD638
@@ -10017,6 +10083,26 @@ var AgentSession = class {
10017
10083
  this.openDocuments.push(p);
10018
10084
  }
10019
10085
  }
10086
+ /**
10087
+ * 저장된 메시지(어시스턴트 tool-call 파트)에서 열람·작성한 문서 경로를 도출해 기록한다.
10088
+ * 세션이 턴마다 재생성되므로, 시스템 프롬프트("열람한 문서")가 이전 턴의 열람 기록을
10089
+ * 반영하려면 히스토리에서 다시 복원해야 한다.
10090
+ */
10091
+ recordOpenDocumentsFromMessage(msg) {
10092
+ const content = msg.content;
10093
+ if (!Array.isArray(content)) return;
10094
+ for (const part of content) {
10095
+ if (!part || typeof part !== "object") continue;
10096
+ const p = part;
10097
+ if (p.type !== "tool-call" || !p.input) continue;
10098
+ if (p.toolName === "read_document" || p.toolName === "write_new_document" || p.toolName === "write_new_spreadsheet") {
10099
+ this.recordOpenDocument(p.input.path);
10100
+ } else if (p.toolName === "compare_documents") {
10101
+ this.recordOpenDocument(p.input.pathA);
10102
+ this.recordOpenDocument(p.input.pathB);
10103
+ }
10104
+ }
10105
+ }
10020
10106
  /** approval-required 이벤트를 run() 스트림에 전달하기 위한 큐 */
10021
10107
  pendingApprovalEvents = [];
10022
10108
  /**
@@ -10025,6 +10111,9 @@ var AgentSession = class {
10025
10111
  async loadHistory() {
10026
10112
  const msgs = await this.opts.store.loadMessages();
10027
10113
  this.messages.push(...msgs);
10114
+ for (const msg of msgs) {
10115
+ this.recordOpenDocumentsFromMessage(msg);
10116
+ }
10028
10117
  }
10029
10118
  /**
10030
10119
  * 사용자 메시지를 처리하고 에이전트 이벤트를 스트리밍한다.
@@ -10079,7 +10168,7 @@ var AgentSession = class {
10079
10168
  } else if (part.toolName === "compare_documents") {
10080
10169
  this.recordOpenDocument(inp.pathA);
10081
10170
  this.recordOpenDocument(inp.pathB);
10082
- } else if (part.toolName === "write_new_document") {
10171
+ } else if (part.toolName === "write_new_document" || part.toolName === "write_new_spreadsheet") {
10083
10172
  this.recordOpenDocument(inp.path);
10084
10173
  }
10085
10174
  } catch {
@@ -10788,6 +10877,13 @@ var ToolRegistry = class {
10788
10877
  return outcome;
10789
10878
  }
10790
10879
  const { proposal, commit } = outcome;
10880
+ const piiFindings = detectPii(proposal.diff ?? "");
10881
+ if (piiFindings.length > 0) {
10882
+ proposal.warnings = [
10883
+ ...proposal.warnings ?? [],
10884
+ `\uAC1C\uC778\uC815\uBCF4 \uD3EC\uD568: \uC774\uBC88 \uBCC0\uACBD \uC601\uC5ED\uC5D0 ${summarizePii(piiFindings)}\uC774(\uAC00) \uC788\uC2B5\uB2C8\uB2E4. \uC678\uBD80 \uACF5\uC720 \uC2DC \uC8FC\uC758\uD558\uC138\uC694.`
10885
+ ];
10886
+ }
10791
10887
  getEventEmitter()?.(proposal);
10792
10888
  const approvalResult = await approvalHandler(proposal);
10793
10889
  if (!approvalResult.approved) {
@@ -10832,23 +10928,27 @@ import { basename as basename3, extname as extname2, join as join32 } from "path
10832
10928
  var import_jszip = __toESM(require_lib3(), 1);
10833
10929
  var import_jszip2 = __toESM(require_lib3(), 1);
10834
10930
  var import_jszip3 = __toESM(require_lib3(), 1);
10931
+ var import_jszip4 = __toESM(require_lib3(), 1);
10835
10932
  import { z as z3 } from "zod";
10836
10933
  import { readFile as readFile32 } from "fs/promises";
10837
10934
  import { blocksToMarkdown, compare } from "@clazic/kordoc";
10838
10935
  import { z as z22 } from "zod";
10936
+ import { readFile as readFile5 } from "fs/promises";
10937
+ import { extname as extname4 } from "path";
10938
+ import { z as z4 } from "zod";
10839
10939
  import { readFile as readFile4 } from "fs/promises";
10840
10940
  import { extname as extname3 } from "path";
10841
10941
  import { z as z32 } from "zod";
10842
- import { readdir as readdir3, stat as stat3 } from "fs/promises";
10843
- import { extname as extname4, join as join4, relative as relative2 } from "path";
10844
- import { z as z4 } from "zod";
10845
- import { readFile as readFile5 } from "fs/promises";
10942
+ import { readFile as readFile6 } from "fs/promises";
10846
10943
  import { extname as extname5 } from "path";
10847
10944
  import { z as z5 } from "zod";
10848
- import { readFile as readFile6 } from "fs/promises";
10849
- import { extname as extname6 } from "path";
10850
- import { compare as compare2, markdownToHwpx, parse } from "@clazic/kordoc";
10945
+ import { readdir as readdir3, stat as stat3 } from "fs/promises";
10946
+ import { extname as extname6, join as join4, relative as relative2 } from "path";
10851
10947
  import { z as z6 } from "zod";
10948
+ import { readFile as readFile7 } from "fs/promises";
10949
+ import { extname as extname7 } from "path";
10950
+ import { compare as compare2, markdownToHwpx, parse } from "@clazic/kordoc";
10951
+ import { z as z7 } from "zod";
10852
10952
  import {
10853
10953
  Document,
10854
10954
  HeadingLevel,
@@ -10860,37 +10960,45 @@ import {
10860
10960
  TextRun,
10861
10961
  WidthType
10862
10962
  } from "docx";
10863
- import { readFile as readFile7 } from "fs/promises";
10864
- import { extname as extname7 } from "path";
10865
- import { parse as parse2 } from "@clazic/kordoc";
10866
- import { z as z7 } from "zod";
10867
10963
  import { readFile as readFile8 } from "fs/promises";
10868
10964
  import { extname as extname8 } from "path";
10869
- import { extractFormFields, markdownToHwpx as markdownToHwpx2, parse as parse3 } from "@clazic/kordoc";
10870
- var import_jszip4 = __toESM(require_lib3(), 1);
10965
+ import { parse as parse2 } from "@clazic/kordoc";
10871
10966
  import { z as z8 } from "zod";
10872
10967
  import { readFile as readFile9 } from "fs/promises";
10873
10968
  import { extname as extname9 } from "path";
10874
- import ExcelJS from "exceljs";
10969
+ import { extractFormFields, markdownToHwpx as markdownToHwpx2, parse as parse3 } from "@clazic/kordoc";
10875
10970
  import { z as z9 } from "zod";
10876
10971
  import { readFile as readFile10 } from "fs/promises";
10877
- import { extname as extname10 } from "path";
10878
- import { parse as parse4 } from "@clazic/kordoc";
10972
+ import { basename as basename4, extname as extname10 } from "path";
10973
+ var import_jszip5 = __toESM(require_lib3(), 1);
10974
+ var import_jszip6 = __toESM(require_lib3(), 1);
10879
10975
  import { z as z10 } from "zod";
10880
- import { readFile as readFile11, stat as stat4 } from "fs/promises";
10976
+ import { readFile as readFile11 } from "fs/promises";
10881
10977
  import { extname as extname11 } from "path";
10882
- import { parse as parse5 } from "@clazic/kordoc";
10978
+ import ExcelJS from "exceljs";
10883
10979
  import { z as z11 } from "zod";
10884
- import { readFile as fsReadFile, stat as stat5 } from "fs/promises";
10980
+ import { readFile as readFile12 } from "fs/promises";
10981
+ import { extname as extname12 } from "path";
10982
+ import { parse as parse4 } from "@clazic/kordoc";
10885
10983
  import { z as z12 } from "zod";
10984
+ import { readFile as readFile13, stat as stat4 } from "fs/promises";
10985
+ import { extname as extname13 } from "path";
10986
+ import { parse as parse5 } from "@clazic/kordoc";
10987
+ import { z as z13 } from "zod";
10988
+ import { readFile as fsReadFile, stat as stat5 } from "fs/promises";
10989
+ import { z as z14 } from "zod";
10990
+ import { readFile as readFile14 } from "fs/promises";
10991
+ import { extname as extname14 } from "path";
10992
+ import { parse as parse6 } from "@clazic/kordoc";
10993
+ import { z as z15 } from "zod";
10886
10994
  import { stat as stat6 } from "fs/promises";
10887
- import { extname as extname12 } from "path";
10995
+ import { extname as extname15 } from "path";
10888
10996
  import { markdownToHwpx as markdownToHwpx3 } from "@clazic/kordoc";
10889
- import { z as z13 } from "zod";
10997
+ import { z as z16 } from "zod";
10890
10998
  import { stat as stat7 } from "fs/promises";
10891
- import { extname as extname13 } from "path";
10999
+ import { extname as extname16 } from "path";
10892
11000
  import ExcelJS2 from "exceljs";
10893
- import { z as z14 } from "zod";
11001
+ import { z as z17 } from "zod";
10894
11002
  async function resolveSafePath(cwd, p) {
10895
11003
  const normalizedCwd = normalize(cwd).normalize("NFC");
10896
11004
  const normalizedP = p.normalize("NFC");
@@ -11155,19 +11263,29 @@ var restoreBackupTool = {
11155
11263
  if (candidates.length === 0) {
11156
11264
  return `\uBC31\uC5C5\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${targetBase}. list_backups\uB85C \uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uBC31\uC5C5\uC744 \uBA3C\uC800 \uD655\uC778\uD558\uC138\uC694.`;
11157
11265
  }
11266
+ const byNewest = (a, b) => b.mtimeMs !== a.mtimeMs ? b.mtimeMs - a.mtimeMs : b.tsToken.localeCompare(a.tsToken);
11158
11267
  let chosen;
11159
- if (input.backup) {
11160
- const found = candidates.find(
11161
- (c) => c.filename === input.backup || c.fullPath.endsWith(input.backup)
11162
- );
11163
- if (!found) {
11164
- return `\uC9C0\uC815\uD55C \uBC31\uC5C5\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.backup}. list_backups\uB85C \uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uBC31\uC5C5\uC744 \uBA3C\uC800 \uD655\uC778\uD558\uC138\uC694.`;
11268
+ let ambiguityNote = null;
11269
+ const requested = input.backup?.trim();
11270
+ if (requested) {
11271
+ const exact = candidates.find((c) => c.filename === requested);
11272
+ if (exact) {
11273
+ chosen = exact;
11274
+ } else {
11275
+ const matches = candidates.filter(
11276
+ (c) => c.filename.endsWith(requested) || c.fullPath.endsWith(requested)
11277
+ );
11278
+ if (matches.length === 0) {
11279
+ return `\uC9C0\uC815\uD55C \uBC31\uC5C5\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.backup}. list_backups\uB85C \uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uBC31\uC5C5\uC758 \uC815\uD655\uD55C \uD30C\uC77C\uBA85\uC744 \uD655\uC778\uD558\uC138\uC694.`;
11280
+ }
11281
+ matches.sort(byNewest);
11282
+ chosen = matches[0];
11283
+ if (matches.length > 1) {
11284
+ ambiguityNote = `'${input.backup}'\uC640 \uC77C\uCE58\uD558\uB294 \uBC31\uC5C5\uC774 ${matches.length}\uAC1C\uC5EC\uC11C \uAC00\uC7A5 \uCD5C\uADFC(${formatTimestamp(chosen.tsToken)}) \uAC83\uC744 \uC120\uD0DD\uD588\uC2B5\uB2C8\uB2E4. \uD2B9\uC815 \uBC31\uC5C5\uC744 \uC6D0\uD558\uBA74 list_backups\uC758 \uC804\uCCB4 \uD30C\uC77C\uBA85\uC744 \uC9C0\uC815\uD558\uC138\uC694.`;
11285
+ }
11165
11286
  }
11166
- chosen = found;
11167
11287
  } else {
11168
- candidates.sort(
11169
- (a, b) => b.mtimeMs !== a.mtimeMs ? b.mtimeMs - a.mtimeMs : b.tsToken.localeCompare(a.tsToken)
11170
- );
11288
+ candidates.sort(byNewest);
11171
11289
  chosen = candidates[0];
11172
11290
  }
11173
11291
  let backupBytes;
@@ -11208,12 +11326,15 @@ var restoreBackupTool = {
11208
11326
  ].join("\n");
11209
11327
  }
11210
11328
  const warnings = [];
11211
- const autoSelected = !input.backup && candidates.length > 1;
11329
+ const autoSelected = !requested && candidates.length > 1;
11212
11330
  if (autoSelected) {
11213
11331
  warnings.push(
11214
11332
  `\uBC31\uC5C5\uC744 \uC9C0\uC815\uD558\uC9C0 \uC54A\uC544 \uAC00\uC7A5 \uCD5C\uADFC \uBC31\uC5C5(${formatTimestamp(chosen.tsToken)})\uC744 \uC790\uB3D9\uC73C\uB85C \uC120\uD0DD\uD588\uC2B5\uB2C8\uB2E4. \uB2E4\uB978 \uBC31\uC5C5\uC744 \uC6D0\uD558\uBA74 list_backups\uB85C \uD655\uC778 \uD6C4 backup \uD30C\uB77C\uBBF8\uD130\uB97C \uC9C0\uC815\uD558\uC138\uC694.`
11215
11333
  );
11216
11334
  }
11335
+ if (ambiguityNote) {
11336
+ warnings.push(ambiguityNote);
11337
+ }
11217
11338
  warnings.push(
11218
11339
  "\uBCF5\uC6D0\uC744 \uC2E4\uD589\uD558\uBA74 \uD604\uC7AC \uD30C\uC77C\uB3C4 \uBC31\uC5C5\uB41C \uB4A4 \uB36E\uC5B4\uC4F0\uC5EC\uC9D1\uB2C8\uB2E4(\uBCF5\uC6D0 \uC790\uCCB4\uB3C4 \uB418\uB3CC\uB9B4 \uC218 \uC788\uC74C)."
11219
11340
  );
@@ -11377,458 +11498,357 @@ var compareDocumentsTool = {
11377
11498
  return output;
11378
11499
  }
11379
11500
  };
11380
- function escapeXml(text3) {
11381
- return text3.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
11382
- }
11383
- function decodeXml(text3) {
11384
- return text3.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'");
11501
+ var cellEditItemSchema = z32.object({
11502
+ tableIndex: z32.number().int().nonnegative().optional().describe(
11503
+ "\uC88C\uD45C \uBAA8\uB4DC: 0-based \uD45C \uC778\uB371\uC2A4(read_document\uC758 kordoc \uBE14\uB85D \uC21C\uC11C, \uC911\uCCA9\uD45C \uC81C\uC678). \uB808\uC774\uBE14 \uBAA8\uB4DC\uC5D0\uC120 \uD0D0\uC0C9 \uBC94\uC704 \uC81C\uD55C\uC6A9(\uC120\uD0DD)."
11504
+ ),
11505
+ row: z32.number().int().nonnegative().optional().describe("\uC88C\uD45C \uBAA8\uB4DC: \uC140\uC758 rowAddr (0-based)"),
11506
+ col: z32.number().int().nonnegative().optional().describe("\uC88C\uD45C \uBAA8\uB4DC: \uC140\uC758 colAddr (0-based)"),
11507
+ label: z32.string().optional().describe(
11508
+ "\uB808\uC774\uBE14 \uBAA8\uB4DC: \uAE30\uC900 \uC140 \uD14D\uC2A4\uD2B8(\uD2B8\uB9BC \uBE44\uAD50). \uC774 \uC140\uC758 direction \uBC29\uD5A5 \uC778\uC811 \uC140\uC5D0 newText\uB97C \uAE30\uB85D. \uC88C\uD45C \uBAA8\uB4DC\uBA74 \uC0DD\uB7B5."
11509
+ ),
11510
+ direction: z32.enum(["right", "below"]).optional().describe("\uB808\uC774\uBE14 \uBAA8\uB4DC \uBC29\uD5A5. \uAE30\uBCF8 right(\uC624\uB978\uCABD \uC140), below(\uC544\uB798 \uC140). \uBCD1\uD569 span \uACE0\uB824."),
11511
+ newText: z32.string().describe("\uC140\uC5D0 \uC4F8 \uC0C8 \uD14D\uC2A4\uD2B8"),
11512
+ expectedText: z32.string().optional().describe(
11513
+ "\uD604\uC7AC \uC140 \uD14D\uC2A4\uD2B8(\uC548\uC804 \uAC80\uC99D\uC6A9). \uBD88\uC77C\uCE58 \uC2DC \uC218\uC815\uD558\uC9C0 \uC54A\uC74C. \uC798\uBABB\uB41C \uC140 \uC218\uC815 \uBC29\uC9C0\uB97C \uC704\uD574 \uAD8C\uC7A5."
11514
+ )
11515
+ }).describe(
11516
+ "\uD3B8\uC9D1 \uD56D\uBAA9. \uC88C\uD45C \uBAA8\uB4DC(tableIndex+row+col) \uB610\uB294 \uB808\uC774\uBE14 \uBAA8\uB4DC(label[+direction]) \uC911 \uD558\uB098\uB97C \uC0AC\uC6A9\uD558\uC138\uC694. \uB458 \uB2E4 \uC9C0\uC815\uD558\uAC70\uB098 \uB458 \uB2E4 \uC0DD\uB7B5\uD558\uBA74 \uC624\uB958\uC785\uB2C8\uB2E4."
11517
+ );
11518
+ var proposeCellEditSchema = z32.object({
11519
+ path: z32.string().describe("\uC218\uC815\uD560 .hwpx \uD30C\uC77C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
11520
+ edits: z32.array(cellEditItemSchema).min(1).describe(
11521
+ "\uD3B8\uC9D1 \uBAA9\uB85D. \uAC01 \uD56D\uBAA9\uC740 \uC88C\uD45C \uBAA8\uB4DC(tableIndex+row+col) \uB610\uB294 \uB808\uC774\uBE14 \uBAA8\uB4DC(label+direction) \uC911 \uD558\uB098"
11522
+ ),
11523
+ summary: z32.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
11524
+ });
11525
+ function tokenizeHwpxXml(xml) {
11526
+ const tokens = [];
11527
+ const re = /<hp:tbl[\s>]|<\/hp:tbl>|<hp:tc[\s>]|<\/hp:tc>|<hp:t\/>|<hp:t>|<hp:cellAddr[^/>]*|<hp:cellSpan[^/>]*/g;
11528
+ let startPos = 0;
11529
+ let m = re.exec(xml);
11530
+ while (m !== null) {
11531
+ const raw = m[0];
11532
+ const pos = m.index;
11533
+ if (raw.startsWith("<hp:tbl")) {
11534
+ tokens.push({ kind: "tbl_open", pos, end: pos + raw.length });
11535
+ } else if (raw === "</hp:tbl>") {
11536
+ tokens.push({ kind: "tbl_close", pos, end: pos + raw.length });
11537
+ } else if (raw.startsWith("<hp:tc")) {
11538
+ tokens.push({ kind: "tc_open", pos, end: pos + raw.length });
11539
+ } else if (raw === "</hp:tc>") {
11540
+ tokens.push({ kind: "tc_close", pos, end: pos + raw.length });
11541
+ } else if (raw === "<hp:t/>") {
11542
+ tokens.push({ kind: "t_empty", pos, end: pos + raw.length });
11543
+ } else if (raw === "<hp:t>") {
11544
+ tokens.push({ kind: "t_open", pos, end: pos + raw.length });
11545
+ } else if (raw.startsWith("<hp:cellAddr")) {
11546
+ const colM = raw.match(/colAddr="(\d+)"/);
11547
+ const rowM = raw.match(/rowAddr="(\d+)"/);
11548
+ if (colM && rowM) {
11549
+ const selfClose = xml.indexOf("/>", pos);
11550
+ const end = selfClose >= 0 ? selfClose + 2 : pos + raw.length;
11551
+ tokens.push({
11552
+ kind: "cell_addr",
11553
+ pos,
11554
+ end,
11555
+ colAddr: Number(colM[1]),
11556
+ rowAddr: Number(rowM[1])
11557
+ });
11558
+ }
11559
+ } else if (raw.startsWith("<hp:cellSpan")) {
11560
+ const colM = raw.match(/colSpan="(\d+)"/);
11561
+ const rowM = raw.match(/rowSpan="(\d+)"/);
11562
+ const selfClose = xml.indexOf("/>", pos);
11563
+ const end = selfClose >= 0 ? selfClose + 2 : pos + raw.length;
11564
+ tokens.push({
11565
+ kind: "cell_span",
11566
+ pos,
11567
+ end,
11568
+ colSpan: colM ? Number(colM[1]) : 1,
11569
+ rowSpan: rowM ? Number(rowM[1]) : 1
11570
+ });
11571
+ }
11572
+ startPos = re.lastIndex;
11573
+ m = re.exec(xml);
11574
+ }
11575
+ void startPos;
11576
+ return tokens;
11385
11577
  }
11386
- function getAttr(openTag, attr) {
11387
- const re = new RegExp(`\\b${attr}="([^"]*)"`, "");
11388
- const m = re.exec(openTag);
11389
- return m ? decodeXml(m[1] ?? "") : "";
11578
+ function escapeXml(text3) {
11579
+ return text3.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
11390
11580
  }
11391
- function parseFormObjects(xml, sectionFile, startIndex = 0) {
11392
- const results = [];
11393
- let idx = startIndex;
11394
- const tagSpecs = [
11395
- { xmlTag: "hp:btn", type: "button" },
11396
- { xmlTag: "hp:checkBtn", type: "checkBox" },
11397
- { xmlTag: "hp:radioBtn", type: "radioButton" },
11398
- { xmlTag: "hp:comboBox", type: "comboBox" },
11399
- { xmlTag: "hp:edit", type: "edit" }
11400
- ];
11401
- const hits = [];
11402
- for (const spec of tagSpecs) {
11403
- const re = new RegExp(`<${spec.xmlTag}\\b`, "g");
11404
- for (let m = re.exec(xml); m !== null; m = re.exec(xml)) {
11405
- hits.push({ pos: m.index, xmlTag: spec.xmlTag, type: spec.type });
11581
+ function collectDirectTcRanges(tokens, tblStart, tblEnd) {
11582
+ const tblTokens = tokens.filter((t) => t.pos > tblStart && t.pos < tblEnd);
11583
+ const tcRanges = [];
11584
+ const tcStack = [];
11585
+ let innerDepth = 0;
11586
+ for (const tok of tblTokens) {
11587
+ if (tok.kind === "tbl_open") {
11588
+ innerDepth++;
11589
+ } else if (tok.kind === "tbl_close") {
11590
+ innerDepth--;
11591
+ } else if (tok.kind === "tc_open") {
11592
+ tcStack.push({ pos: tok.pos, depth: innerDepth });
11593
+ } else if (tok.kind === "tc_close") {
11594
+ const entry = tcStack.pop();
11595
+ if (entry !== void 0) {
11596
+ tcRanges.push({ start: entry.pos, end: tok.end, depth: entry.depth });
11597
+ }
11406
11598
  }
11407
11599
  }
11408
- hits.sort((a, b) => a.pos - b.pos);
11409
- for (const hit of hits) {
11410
- const { pos, xmlTag, type } = hit;
11411
- const openTagEnd = xml.indexOf(">", pos);
11412
- if (openTagEnd < 0) continue;
11413
- const openTag = xml.slice(pos, openTagEnd + 1);
11414
- const closeTag = `</${xmlTag}>`;
11415
- const closeIdx = xml.indexOf(closeTag, openTagEnd);
11416
- if (closeIdx < 0) continue;
11417
- const elementEnd = closeIdx + closeTag.length;
11418
- const elementXml = xml.slice(pos, elementEnd);
11419
- const name = getAttr(openTag, "name");
11420
- let currentValue;
11421
- let comboItems;
11422
- switch (type) {
11423
- case "button":
11424
- currentValue = getAttr(openTag, "caption");
11425
- break;
11426
- case "checkBox":
11427
- case "radioButton":
11428
- currentValue = getAttr(openTag, "value") === "CHECKED";
11429
- break;
11430
- case "comboBox": {
11431
- currentValue = getAttr(openTag, "selectedValue");
11432
- const listItemRe = /<hp:listItem\b[^>]*/g;
11433
- const items = [];
11434
- for (let lm = listItemRe.exec(elementXml); lm !== null; lm = listItemRe.exec(elementXml)) {
11435
- const itemValue = getAttr(lm[0] ?? "", "value");
11436
- items.push(itemValue);
11437
- }
11438
- comboItems = items;
11439
- break;
11440
- }
11441
- case "edit": {
11442
- const textSelfClose = /<hp:text\s*\/>/;
11443
- const textOpen = /<hp:text>/;
11444
- const textClose = "</hp:text>";
11445
- if (textSelfClose.test(elementXml)) {
11446
- currentValue = "";
11447
- } else {
11448
- const tOpenIdx = elementXml.search(textOpen);
11449
- if (tOpenIdx >= 0) {
11450
- const afterOpen = elementXml.indexOf(">", tOpenIdx) + 1;
11451
- const closePos = elementXml.indexOf(textClose, afterOpen);
11452
- currentValue = closePos >= 0 ? decodeXml(elementXml.slice(afterOpen, closePos)) : "";
11453
- } else {
11454
- currentValue = "";
11455
- }
11456
- }
11457
- break;
11600
+ return tcRanges.filter((tc) => tc.depth === 0);
11601
+ }
11602
+ function readOwnTextFromTc(xml, tokens, tcStart, tcEnd) {
11603
+ const tcTokens = tokens.filter((t) => t.pos >= tcStart && t.pos < tcEnd);
11604
+ let innerDepth = 0;
11605
+ let text3 = "";
11606
+ for (const t of tcTokens) {
11607
+ if (t.kind === "tbl_open") innerDepth++;
11608
+ else if (t.kind === "tbl_close") innerDepth--;
11609
+ else if (t.kind === "t_empty" && innerDepth === 0) {
11610
+ } else if (t.kind === "t_open" && innerDepth === 0) {
11611
+ const closePos = xml.indexOf("</hp:t>", t.end);
11612
+ if (closePos >= 0) {
11613
+ text3 += xml.substring(t.end, closePos);
11458
11614
  }
11459
11615
  }
11460
- results.push({
11461
- index: idx++,
11462
- name,
11463
- type,
11464
- currentValue,
11465
- comboItems,
11466
- sectionFile,
11467
- posInSection: pos
11468
- });
11469
11616
  }
11470
- return results;
11617
+ return text3.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
11471
11618
  }
11472
- function applyFormObjectEdits(xml, edits) {
11473
- const results = edits.map(() => ({ success: false }));
11474
- const patches = [];
11619
+ function findTopLevelTableRange(tokens, tableIndex) {
11620
+ let topLevelCount = 0;
11621
+ let depth = 0;
11622
+ let targetStart = -1;
11623
+ for (const tok of tokens) {
11624
+ if (tok.kind === "tbl_open") {
11625
+ if (depth === 0) {
11626
+ if (topLevelCount === tableIndex) {
11627
+ targetStart = tok.pos;
11628
+ }
11629
+ topLevelCount++;
11630
+ }
11631
+ depth++;
11632
+ } else if (tok.kind === "tbl_close") {
11633
+ depth--;
11634
+ if (depth === 0 && topLevelCount === tableIndex + 1 && targetStart >= 0) {
11635
+ return { start: targetStart, end: tok.end };
11636
+ }
11637
+ }
11638
+ }
11639
+ return null;
11640
+ }
11641
+ function readCellAddrSpan(tokens, tcStart, tcEnd) {
11642
+ const tcTokens = tokens.filter((t) => t.pos >= tcStart && t.pos < tcEnd);
11643
+ let addr = null;
11644
+ let span = { colSpan: 1, rowSpan: 1 };
11645
+ for (const t of tcTokens) {
11646
+ if (t.kind === "cell_addr" && t.colAddr !== void 0 && t.rowAddr !== void 0) {
11647
+ addr = { colAddr: t.colAddr, rowAddr: t.rowAddr };
11648
+ } else if (t.kind === "cell_span" && t.colSpan !== void 0 && t.rowSpan !== void 0) {
11649
+ span = { colSpan: t.colSpan, rowSpan: t.rowSpan };
11650
+ }
11651
+ }
11652
+ if (!addr) return null;
11653
+ return { ...addr, ...span };
11654
+ }
11655
+ function applyCellEditsToSectionXml(xml, edits) {
11656
+ const tokens = tokenizeHwpxXml(xml);
11657
+ const results = edits.map(() => ({ success: false }));
11658
+ let totalTopLevelTbls = 0;
11659
+ {
11660
+ let d = 0;
11661
+ for (const tok of tokens) {
11662
+ if (tok.kind === "tbl_open") {
11663
+ if (d === 0) totalTopLevelTbls++;
11664
+ d++;
11665
+ } else if (tok.kind === "tbl_close") {
11666
+ d--;
11667
+ }
11668
+ }
11669
+ }
11670
+ const replacements = [];
11475
11671
  for (let ei = 0; ei < edits.length; ei++) {
11476
11672
  const edit = edits[ei];
11477
- const { target, set, expected } = edit;
11478
- const pos = target.posInSection;
11479
- const xmlTag = typeToXmlTag(target.type);
11480
- const openTagEnd = xml.indexOf(">", pos);
11481
- if (openTagEnd < 0) {
11482
- results[ei] = {
11483
- success: false,
11484
- error: `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}" \uC5EC\uB294 \uD0DC\uADF8 \uB05D\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
11485
- };
11486
- continue;
11487
- }
11488
- const _openTag = xml.slice(pos, openTagEnd + 1);
11489
- const closeTag = `</${xmlTag}>`;
11490
- const closeIdx = xml.indexOf(closeTag, openTagEnd);
11491
- if (closeIdx < 0) {
11673
+ const tblRange = findTopLevelTableRange(tokens, edit.tableIndex);
11674
+ if (!tblRange) {
11492
11675
  results[ei] = {
11493
11676
  success: false,
11494
- error: `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}" \uB2EB\uB294 \uD0DC\uADF8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
11677
+ error: `\uD45C ${edit.tableIndex}\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774 \uC139\uC158\uC5D0 \uCD1D ${totalTopLevelTbls}\uAC1C\uC758 \uCD5C\uC0C1\uC704 \uD45C\uAC00 \uC788\uC2B5\uB2C8\uB2E4.`
11495
11678
  };
11496
11679
  continue;
11497
11680
  }
11498
- const elementEnd = closeIdx + closeTag.length;
11499
- const typeError = validateSetForType(target.type, set);
11500
- if (typeError) {
11501
- results[ei] = { success: false, error: typeError };
11502
- continue;
11503
- }
11504
- const currentRaw = readCurrentValue(
11505
- xml,
11506
- pos,
11507
- openTagEnd,
11508
- openTagEnd + 1,
11509
- elementEnd,
11510
- target.type
11511
- );
11512
- if (expected !== void 0) {
11513
- const mismatch = checkExpected(expected, currentRaw, target);
11514
- if (mismatch) {
11515
- results[ei] = { success: false, error: mismatch };
11516
- continue;
11517
- }
11518
- }
11519
- if (target.type === "comboBox" && set.selected !== void 0) {
11520
- const items = target.comboItems ?? [];
11521
- if (!items.includes(set.selected)) {
11522
- const validList = items.map((v) => `"${v}"`).join(", ");
11523
- results[ei] = {
11524
- success: false,
11525
- error: `comboBox "${target.name}"\uC758 selected \uAC12 "${set.selected}"\uC774 \uC720\uD6A8\uD55C \uD56D\uBAA9\uC774 \uC544\uB2D9\uB2C8\uB2E4. \uC720\uD6A8\uD55C \uD56D\uBAA9: ${validList || "(\uC5C6\uC74C)"}`
11526
- };
11527
- continue;
11681
+ const directTcs = collectDirectTcRanges(tokens, tblRange.start, tblRange.end);
11682
+ const tblTokens = tokens.filter((t) => t.pos >= tblRange.start && t.pos < tblRange.end);
11683
+ let found = false;
11684
+ for (const tc of directTcs) {
11685
+ const tcTokens = tblTokens.filter((t) => t.pos >= tc.start && t.pos < tc.end);
11686
+ const hasAddr = tcTokens.some(
11687
+ (t) => t.kind === "cell_addr" && t.colAddr === edit.col && t.rowAddr === edit.row
11688
+ );
11689
+ if (!hasAddr) continue;
11690
+ let innerDepth = 0;
11691
+ const ownTRuns = [];
11692
+ for (const t of tokens.filter((x) => x.pos >= tc.start && x.pos < tc.end)) {
11693
+ if (t.kind === "tbl_open") innerDepth++;
11694
+ else if (t.kind === "tbl_close") innerDepth--;
11695
+ else if (t.kind === "t_empty" && innerDepth === 0) {
11696
+ ownTRuns.push({ isEmpty: true, tagPos: t.pos, tagEnd: t.end });
11697
+ } else if (t.kind === "t_open" && innerDepth === 0) {
11698
+ const closePos = xml.indexOf("</hp:t>", t.end);
11699
+ if (closePos >= 0) {
11700
+ ownTRuns.push({ isEmpty: false, openEnd: t.end, closePos });
11701
+ }
11702
+ }
11528
11703
  }
11529
- }
11530
- let patchCreated = false;
11531
- if (set.caption !== void 0) {
11532
- const patch = replaceAttrInOpenTag(xml, pos, openTagEnd, "caption", escapeXml(set.caption));
11533
- if (!patch) {
11704
+ const currentText = ownTRuns.map((r) => r.isEmpty ? "" : xml.substring(r.openEnd, r.closePos)).join("").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
11705
+ if (edit.expectedText !== void 0 && edit.expectedText !== currentText) {
11534
11706
  results[ei] = {
11535
11707
  success: false,
11536
- error: `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}"\uC758 caption \uC18D\uC131\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
11708
+ oldText: currentText,
11709
+ error: `\uC140 (\uD45C ${edit.tableIndex}, \uD589 ${edit.row}, \uC5F4 ${edit.col})\uC758 \uD604\uC7AC \uD14D\uC2A4\uD2B8\uAC00 \uC608\uC0C1\uAC12\uACFC \uB2E4\uB985\uB2C8\uB2E4. \uC608\uC0C1: "${edit.expectedText}", \uC2E4\uC81C: "${currentText}". \uC218\uC815\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`
11537
11710
  };
11538
- continue;
11711
+ found = true;
11712
+ break;
11539
11713
  }
11540
- patches.push(patch);
11541
- patchCreated = true;
11542
- } else if (set.checked !== void 0) {
11543
- const newValue = set.checked ? "CHECKED" : "UNCHECKED";
11544
- const patch = replaceAttrInOpenTag(xml, pos, openTagEnd, "value", newValue);
11545
- if (!patch) {
11714
+ if (ownTRuns.length === 0) {
11546
11715
  results[ei] = {
11547
11716
  success: false,
11548
- error: `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}"\uC758 value \uC18D\uC131\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
11717
+ oldText: currentText,
11718
+ error: `\uC140 (\uD45C ${edit.tableIndex}, \uD589 ${edit.row}, \uC5F4 ${edit.col})\uC5D0 \uD14D\uC2A4\uD2B8 \uB7F0\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. <hp:t> \uB610\uB294 <hp:t/> \uB7F0\uC774 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uC140 \uAD6C\uC870\uB97C \uD655\uC778\uD558\uC138\uC694.`
11549
11719
  };
11550
- continue;
11720
+ found = true;
11721
+ break;
11551
11722
  }
11552
- patches.push(patch);
11553
- patchCreated = true;
11554
- } else if (set.selected !== void 0) {
11555
- const patch = replaceAttrInOpenTag(
11556
- xml,
11557
- pos,
11558
- openTagEnd,
11559
- "selectedValue",
11560
- escapeXml(set.selected)
11561
- );
11562
- if (!patch) {
11563
- results[ei] = {
11564
- success: false,
11565
- error: `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}"\uC758 selectedValue \uC18D\uC131\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
11566
- };
11567
- continue;
11723
+ const escapedNew = escapeXml(edit.newText);
11724
+ const patches = [];
11725
+ const firstRun = ownTRuns[0];
11726
+ if (firstRun.isEmpty) {
11727
+ patches.push({
11728
+ from: firstRun.tagPos,
11729
+ to: firstRun.tagEnd,
11730
+ text: `<hp:t>${escapedNew}</hp:t>`
11731
+ });
11732
+ } else {
11733
+ patches.push({ from: firstRun.openEnd, to: firstRun.closePos, text: escapedNew });
11568
11734
  }
11569
- patches.push(patch);
11570
- patchCreated = true;
11571
- } else if (set.text !== void 0) {
11572
- const elementContent = xml.slice(pos, elementEnd);
11573
- const textPatch = replaceEditText(xml, pos, elementContent, set.text);
11574
- if (!textPatch) {
11575
- results[ei] = {
11576
- success: false,
11577
- error: `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}"\uC758 <hp:text> \uC694\uC18C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
11578
- };
11579
- continue;
11735
+ for (let ri = 1; ri < ownTRuns.length; ri++) {
11736
+ const run = ownTRuns[ri];
11737
+ if (!run.isEmpty) {
11738
+ patches.push({ from: run.openEnd, to: run.closePos, text: "" });
11739
+ }
11580
11740
  }
11581
- patches.push(textPatch);
11582
- patchCreated = true;
11741
+ replacements.push({ editIdx: ei, patches });
11742
+ results[ei] = { success: true, oldText: currentText };
11743
+ found = true;
11744
+ break;
11583
11745
  }
11584
- if (!patchCreated) {
11585
- results[ei] = { success: false, error: `\uD3B8\uC9D1 #${ei + 1}: set \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.` };
11586
- continue;
11746
+ if (!found) {
11747
+ results[ei] = {
11748
+ success: false,
11749
+ error: `\uD45C ${edit.tableIndex}\uC5D0\uC11C \uC140 (\uD589 ${edit.row}, \uC5F4 ${edit.col})\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. cellAddr colAddr="${edit.col}" rowAddr="${edit.row}"\uC5D0 \uD574\uB2F9\uD558\uB294 \uC140\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`
11750
+ };
11587
11751
  }
11588
- results[ei] = { success: true, oldValue: currentRaw };
11589
11752
  }
11590
11753
  if (results.some((r) => !r.success)) {
11591
11754
  return { newXml: xml, results };
11592
11755
  }
11593
- const sortedPatches = [...patches].sort((a, b) => b.from - a.from);
11756
+ const allPatches = replacements.flatMap((r) => r.patches).sort((a, b) => b.from - a.from);
11594
11757
  let result = xml;
11595
- for (const patch of sortedPatches) {
11596
- result = result.slice(0, patch.from) + patch.text + result.slice(patch.to);
11758
+ for (const patch of allPatches) {
11759
+ result = result.substring(0, patch.from) + patch.text + result.substring(patch.to);
11597
11760
  }
11598
11761
  return { newXml: result, results };
11599
11762
  }
11600
- function typeToXmlTag(type) {
11601
- switch (type) {
11602
- case "button":
11603
- return "hp:btn";
11604
- case "checkBox":
11605
- return "hp:checkBtn";
11606
- case "radioButton":
11607
- return "hp:radioBtn";
11608
- case "comboBox":
11609
- return "hp:comboBox";
11610
- case "edit":
11611
- return "hp:edit";
11763
+ async function applyEditsToHwpx(hwpxBuffer, edits) {
11764
+ const zip = await import_jszip2.default.loadAsync(hwpxBuffer);
11765
+ const sectionFiles = Object.keys(zip.files).filter((name) => /^Contents\/section\d+\.xml$/.test(name)).sort();
11766
+ const sectionXmls = [];
11767
+ const sectionTblCounts = [];
11768
+ for (const sf of sectionFiles) {
11769
+ const entry = zip.file(sf);
11770
+ const xml = entry ? await entry.async("string") : "";
11771
+ sectionXmls.push(xml);
11772
+ const tokens = tokenizeHwpxXml(xml);
11773
+ let count = 0;
11774
+ let depth = 0;
11775
+ for (const tok of tokens) {
11776
+ if (tok.kind === "tbl_open") {
11777
+ if (depth === 0) count++;
11778
+ depth++;
11779
+ } else if (tok.kind === "tbl_close") {
11780
+ depth--;
11781
+ }
11782
+ }
11783
+ sectionTblCounts.push(count);
11612
11784
  }
11613
- }
11614
- function validateSetForType(type, set) {
11615
- const key = Object.keys(set).find((k) => set[k] !== void 0);
11616
- if (!key) return "set \uD544\uB4DC\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. caption/checked/selected/text \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD558\uC138\uC694.";
11617
- const allowed = {
11618
- button: "caption",
11619
- checkBox: "checked",
11620
- radioButton: "checked",
11621
- comboBox: "selected",
11622
- edit: "text"
11623
- };
11624
- if (key !== allowed[type]) {
11625
- const typeKo = {
11626
- button: "PushButton",
11627
- checkBox: "CheckBox",
11628
- radioButton: "RadioButton",
11629
- comboBox: "ComboBox",
11630
- edit: "Edit"
11631
- };
11632
- return `\uD0C0\uC785 \uBD88\uC77C\uCE58: ${typeKo[type]} \uC591\uC2DD \uAC1C\uCCB4\uC5D0\uB294 "${allowed[type]}" \uD544\uB4DC\uB9CC \uC0AC\uC6A9 \uAC00\uB2A5\uD558\uC9C0\uB9CC "${key}"\uC774(\uAC00) \uC9C0\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`;
11633
- }
11634
- return null;
11635
- }
11636
- function readCurrentValue(xml, pos, openTagEnd, _afterOpen, elementEnd, type) {
11637
- const openTag = xml.slice(pos, openTagEnd + 1);
11638
- switch (type) {
11639
- case "button":
11640
- return decodeXml(getAttr(openTag, "caption"));
11641
- case "checkBox":
11642
- case "radioButton":
11643
- return getAttr(openTag, "value") === "CHECKED";
11644
- case "comboBox":
11645
- return decodeXml(getAttr(openTag, "selectedValue"));
11646
- case "edit": {
11647
- const elementContent = xml.slice(pos, elementEnd);
11648
- if (/<hp:text\s*\/>/.test(elementContent)) return "";
11649
- const tOpenIdx = elementContent.search(/<hp:text>/);
11650
- if (tOpenIdx >= 0) {
11651
- const afterOpen = elementContent.indexOf(">", tOpenIdx) + 1;
11652
- const closePos = elementContent.indexOf("</hp:text>", afterOpen);
11653
- return closePos >= 0 ? decodeXml(elementContent.slice(afterOpen, closePos)) : "";
11785
+ const sectionEdits = sectionFiles.map(() => []);
11786
+ let offset = 0;
11787
+ for (let si = 0; si < sectionFiles.length; si++) {
11788
+ const count = sectionTblCounts[si] ?? 0;
11789
+ for (let ei = 0; ei < edits.length; ei++) {
11790
+ const edit = edits[ei];
11791
+ if (edit.tableIndex >= offset && edit.tableIndex < offset + count) {
11792
+ const secEdits = sectionEdits[si];
11793
+ if (secEdits) {
11794
+ secEdits.push({
11795
+ tableIndex: edit.tableIndex - offset,
11796
+ // 섹션 상대 인덱스
11797
+ row: edit.row,
11798
+ col: edit.col,
11799
+ newText: edit.newText,
11800
+ expectedText: edit.expectedText,
11801
+ originalEditIdx: ei
11802
+ });
11803
+ }
11654
11804
  }
11655
- return "";
11656
11805
  }
11806
+ offset += count;
11657
11807
  }
11658
- }
11659
- function checkExpected(expected, currentRaw, target) {
11660
- let expectedVal;
11661
- if (expected.caption !== void 0) expectedVal = expected.caption;
11662
- else if (expected.checked !== void 0) expectedVal = expected.checked;
11663
- else if (expected.selected !== void 0) expectedVal = expected.selected;
11664
- else if (expected.text !== void 0) expectedVal = expected.text;
11665
- if (expectedVal === void 0) return null;
11666
- const matches = typeof expectedVal === "boolean" ? currentRaw === expectedVal : String(currentRaw) === String(expectedVal);
11667
- if (!matches) {
11668
- return `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}" \uD604\uC7AC \uAC12\uC774 \uC608\uC0C1\uAC12\uACFC \uB2E4\uB985\uB2C8\uB2E4. \uC608\uC0C1: ${JSON.stringify(expectedVal)}, \uC2E4\uC81C: ${JSON.stringify(currentRaw)}. \uC218\uC815\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`;
11669
- }
11670
- return null;
11671
- }
11672
- function replaceAttrInOpenTag(xml, tagStart, tagEnd, attr, newValue) {
11673
- const tagStr = xml.slice(tagStart, tagEnd + 1);
11674
- const re = new RegExp(`\\b(${attr}=")([^"]*)(")`, "");
11675
- const m = re.exec(tagStr);
11676
- if (!m) return null;
11677
- const relFrom = m.index + (m[1]?.length ?? 0);
11678
- const relTo = relFrom + (m[2]?.length ?? 0);
11679
- return {
11680
- from: tagStart + relFrom,
11681
- to: tagStart + relTo,
11682
- text: newValue
11683
- };
11684
- }
11685
- function replaceEditText(_xml, pos, elementContent, newText) {
11686
- const escaped = escapeXml(newText);
11687
- const selfCloseRe = /<hp:text\s*\/>/;
11688
- const scm = selfCloseRe.exec(elementContent);
11689
- if (scm) {
11690
- return {
11691
- from: pos + scm.index,
11692
- to: pos + scm.index + scm[0].length,
11693
- text: `<hp:text>${escaped}</hp:text>`
11694
- };
11695
- }
11696
- const openRe = /<hp:text>/;
11697
- const om = openRe.exec(elementContent);
11698
- if (om) {
11699
- const afterOpen = om.index + om[0].length;
11700
- const closePos = elementContent.indexOf("</hp:text>", afterOpen);
11701
- if (closePos < 0) return null;
11702
- return {
11703
- from: pos + afterOpen,
11704
- to: pos + closePos,
11705
- text: escaped
11706
- };
11707
- }
11708
- return null;
11709
- }
11710
- async function listFormObjectsFromZip(zip) {
11711
- const sectionFiles = Object.keys(zip.files).filter((name) => /^Contents\/section\d+\.xml$/.test(name)).sort();
11712
- const sectionXmls = [];
11713
- const objects = [];
11714
- let globalIndex = 0;
11715
- for (const sf of sectionFiles) {
11716
- const entry = zip.file(sf);
11717
- const xml = entry ? await entry.async("string") : "";
11718
- sectionXmls.push(xml);
11719
- const parsed = parseFormObjects(xml, sf, globalIndex);
11720
- objects.push(...parsed);
11721
- globalIndex += parsed.length;
11722
- }
11723
- return { objects, sectionFiles, sectionXmls };
11724
- }
11725
- function validateHwpxBuffer(ext, buffer) {
11726
- if (ext === ".hwp") {
11727
- return "\uC624\uB958: \uC774 \uD234\uC740 .hwpx \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. .hwp(\uAD6C\uD615 OLE \uBC14\uC774\uB108\uB9AC)\uB294 \uC9C1\uC811 \uD3B8\uC9D1\uC774 \uBD88\uAC00\uD569\uB2C8\uB2E4. \uD55C\uAE00 \uD504\uB85C\uADF8\uB7A8\uC5D0\uC11C '\uB2E4\uB978 \uC774\uB984\uC73C\uB85C \uC800\uC7A5 \u2192 .hwpx'\uB85C \uC800\uC7A5\uD55C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.";
11808
+ const totalTables = sectionTblCounts.reduce((a, b) => a + b, 0);
11809
+ const allResults = edits.map((edit, ei) => ({
11810
+ success: false,
11811
+ error: `\uD45C ${edit?.tableIndex ?? ei}\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uC11C\uC5D0 \uCD1D ${totalTables}\uAC1C\uC758 \uCD5C\uC0C1\uC704 \uD45C\uAC00 \uC788\uC2B5\uB2C8\uB2E4.`
11812
+ }));
11813
+ const newSectionXmls = [...sectionXmls];
11814
+ for (let si = 0; si < sectionFiles.length; si++) {
11815
+ const sEdits = sectionEdits[si] ?? [];
11816
+ if (sEdits.length === 0) continue;
11817
+ const srcXml = sectionXmls[si] ?? "";
11818
+ const { newXml, results } = applyCellEditsToSectionXml(srcXml, sEdits);
11819
+ newSectionXmls[si] = newXml;
11820
+ for (let i = 0; i < sEdits.length; i++) {
11821
+ const sEdit = sEdits[i];
11822
+ const res = results[i];
11823
+ if (sEdit && res) {
11824
+ allResults[sEdit.originalEditIdx] = res;
11825
+ }
11826
+ }
11728
11827
  }
11729
- if (ext !== ".hwpx") {
11730
- return `\uC624\uB958: \uC774 \uD234\uC740 .hwpx \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. \uD604\uC7AC \uD30C\uC77C \uD655\uC7A5\uC790: ${ext}`;
11828
+ if (allResults.some((r) => !r.success)) {
11829
+ return { buffer: hwpxBuffer, results: allResults };
11731
11830
  }
11732
- if (buffer[0] !== 80 || buffer[1] !== 75) {
11733
- return "\uC624\uB958: \uD30C\uC77C\uC774 \uC720\uD6A8\uD55C .hwpx(ZIP) \uD3EC\uB9F7\uC774 \uC544\uB2D9\uB2C8\uB2E4. \uD30C\uC77C\uC774 \uC190\uC0C1\uB418\uC5C8\uAC70\uB098 \uAD6C\uD615 .hwp(OLE \uBC14\uC774\uB108\uB9AC) \uD3EC\uB9F7\uC785\uB2C8\uB2E4.";
11831
+ const out = new import_jszip2.default();
11832
+ const mimetypeEntry = zip.file("mimetype");
11833
+ if (mimetypeEntry) {
11834
+ out.file("mimetype", await mimetypeEntry.async("uint8array"), { compression: "STORE" });
11734
11835
  }
11735
- return null;
11736
- }
11737
- var listFormObjectsSchema = z32.object({
11738
- path: z32.string().describe("\uC77D\uC744 .hwpx \uD30C\uC77C \uACBD\uB85C")
11739
- });
11740
- var formEditSetSchema = z32.object({
11741
- caption: z32.string().optional().describe("PushButton \uCEA1\uC158 \uD14D\uC2A4\uD2B8"),
11742
- checked: z32.boolean().optional().describe("CheckBox/RadioButton \uCCB4\uD06C \uC0C1\uD0DC (true=CHECKED)"),
11743
- selected: z32.string().optional().describe("ComboBox \uC120\uD0DD \uAC12 (listItem \uC911 \uD558\uB098\uC5EC\uC57C \uD568)"),
11744
- text: z32.string().optional().describe("Edit \uD14D\uC2A4\uD2B8 \uB0B4\uC6A9")
11745
- }).refine(
11746
- (v) => {
11747
- const keys = ["caption", "checked", "selected", "text"].filter(
11748
- (k) => v[k] !== void 0
11749
- );
11750
- return keys.length === 1;
11751
- },
11752
- { message: "set \uD544\uB4DC\uB294 caption/checked/selected/text \uC911 \uC815\uD655\uD788 \uD558\uB098\uB9CC \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4." }
11753
- );
11754
- var formEditExpectedSchema = z32.object({
11755
- caption: z32.string().optional(),
11756
- checked: z32.boolean().optional(),
11757
- selected: z32.string().optional(),
11758
- text: z32.string().optional()
11759
- }).optional();
11760
- var formEditItemSchema = z32.object({
11761
- name: z32.string().describe("\uC591\uC2DD \uAC1C\uCCB4\uC758 name \uC18D\uC131 \uAC12"),
11762
- index: z32.number().int().nonnegative().optional().describe("\uB3D9\uC77C name\uC774 \uC5EC\uB7FF\uC778 \uACBD\uC6B0 \uBB38\uC11C \uC804\uCCB4 0-based \uC778\uB371\uC2A4\uB85C \uAD6C\uBD84"),
11763
- set: formEditSetSchema.describe("\uBCC0\uACBD\uD560 \uAC12 (caption/checked/selected/text \uC911 \uD558\uB098)"),
11764
- expected: formEditExpectedSchema.describe(
11765
- "\uD604\uC7AC \uAC12 \uC0AC\uC804 \uAC80\uC99D (\uC548\uC804 \uC635\uC158). \uC2E4\uC81C \uAC12\uC774 \uB2E4\uB974\uBA74 \uC774 \uD3B8\uC9D1\uC744 \uCDE8\uC18C\uD558\uACE0 \uC624\uB958 \uBC18\uD658."
11766
- )
11767
- });
11768
- var proposeFormObjectSchema = z32.object({
11769
- path: z32.string().describe("\uC218\uC815\uD560 .hwpx \uD30C\uC77C \uACBD\uB85C"),
11770
- edits: z32.array(formEditItemSchema).min(1).describe("\uC591\uC2DD \uAC1C\uCCB4 \uD3B8\uC9D1 \uBAA9\uB85D"),
11771
- summary: z32.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
11772
- });
11773
- var listFormObjectsTool = {
11774
- name: "list_form_objects",
11775
- description: "HWPX \uBB38\uC11C\uC758 \uC591\uC2DD \uAC1C\uCCB4(form object) \uBAA9\uB85D\uC744 \uC5F4\uAC70\uD569\uB2C8\uB2E4. PushButton, CheckBox, RadioButton, ComboBox, Edit \uB2E4\uC12F \uAC00\uC9C0 \uD0C0\uC785\uC744 \uC9C0\uC6D0\uD569\uB2C8\uB2E4. \uAC01 \uC591\uC2DD \uAC1C\uCCB4\uC758 \uC774\uB984(name), \uD0C0\uC785, \uD604\uC7AC \uAC12, ComboBox\uC758 \uACBD\uC6B0 \uC120\uD0DD \uAC00\uB2A5\uD55C \uD56D\uBAA9 \uBAA9\uB85D\uC744 \uBC18\uD658\uD569\uB2C8\uB2E4. propose_form_object\uB85C \uAC12\uC744 \uC218\uC815\uD558\uAE30 \uC804\uC5D0 \uC774 \uD234\uB85C \uBA3C\uC800 \uD604\uC7AC \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. .hwpx \uD30C\uC77C \uC804\uC6A9\uC785\uB2C8\uB2E4.",
11776
- inputSchema: listFormObjectsSchema,
11777
- requiresApproval: false,
11778
- execute: async ({
11779
- input,
11780
- ctx
11781
- }) => {
11782
- const safePath = await resolveSafePath(ctx.cwd, input.path);
11783
- const ext = extname3(safePath).toLowerCase();
11784
- let buffer;
11785
- try {
11786
- buffer = await readFile4(safePath);
11787
- } catch {
11788
- return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}`;
11789
- }
11790
- const validationError = validateHwpxBuffer(ext, new Uint8Array(buffer.buffer));
11791
- if (validationError) return validationError;
11792
- let zip;
11793
- try {
11794
- zip = await import_jszip.default.loadAsync(new Uint8Array(buffer.buffer));
11795
- } catch {
11796
- return "\uC624\uB958: .hwpx(ZIP) \uD30C\uC77C\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD30C\uC77C\uC774 \uC190\uC0C1\uB418\uC5C8\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.";
11797
- }
11798
- const { objects } = await listFormObjectsFromZip(zip);
11799
- if (objects.length === 0) {
11800
- return "\uC774 \uBB38\uC11C\uC5D0\uB294 \uC591\uC2DD \uAC1C\uCCB4(form object)\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.";
11801
- }
11802
- const lines = [`\uCD1D ${objects.length}\uAC1C\uC758 \uC591\uC2DD \uAC1C\uCCB4\uAC00 \uBC1C\uACAC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
11803
- `];
11804
- const typeKo = {
11805
- button: "PushButton",
11806
- checkBox: "CheckBox",
11807
- radioButton: "RadioButton",
11808
- comboBox: "ComboBox",
11809
- edit: "Edit"
11810
- };
11811
- for (const obj of objects) {
11812
- let valueStr;
11813
- if (typeof obj.currentValue === "boolean") {
11814
- valueStr = obj.currentValue ? "\uCCB4\uD06C\uB428 (CHECKED)" : "\uCCB4\uD06C \uD574\uC81C\uB428 (UNCHECKED)";
11815
- } else {
11816
- valueStr = `"${obj.currentValue}"`;
11817
- }
11818
- let line = `[${obj.index}] name="${obj.name}" | \uD0C0\uC785: ${typeKo[obj.type]} | \uD604\uC7AC \uAC12: ${valueStr}`;
11819
- if (obj.type === "comboBox" && obj.comboItems) {
11820
- const itemList = obj.comboItems.map((v) => `"${v}"`).join(", ");
11821
- line += ` | \uD56D\uBAA9: [${itemList}]`;
11822
- }
11823
- lines.push(line);
11836
+ for (const [name, entry] of Object.entries(zip.files)) {
11837
+ if (name === "mimetype" || entry.dir) continue;
11838
+ const sectionIdx = sectionFiles.indexOf(name);
11839
+ if (sectionIdx >= 0) {
11840
+ out.file(name, newSectionXmls[sectionIdx] ?? "");
11841
+ } else {
11842
+ out.file(name, await entry.async("uint8array"));
11824
11843
  }
11825
- return lines.join("\n");
11826
11844
  }
11827
- };
11828
- var proposeFormObjectTool = {
11829
- name: "propose_form_object",
11830
- description: "HWPX \uBB38\uC11C\uC758 \uC591\uC2DD \uAC1C\uCCB4(form object) \uAC12\uC744 XML \uC9C1\uC811 \uD328\uCE58 \uBC29\uC2DD\uC73C\uB85C \uC218\uC815\uD569\uB2C8\uB2E4. PushButton(caption), CheckBox/RadioButton(checked), ComboBox(selected), Edit(text)\uB97C \uC218\uC815\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. kordoc IR\uC5D0 \uC591\uC2DD \uAC1C\uCCB4 \uD0C0\uC785\uC774 \uC5C6\uC73C\uBBC0\uB85C section XML\uC744 \uC9C1\uC811 \uD328\uCE58\uD569\uB2C8\uB2E4. \uC218\uC815 \uC804\uC5D0 list_form_objects\uB85C \uD604\uC7AC \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. name\uC774 \uC911\uBCF5\uB41C \uACBD\uC6B0 index\uB85C \uB300\uC0C1\uC744 \uC9C0\uC815\uD558\uC138\uC694. \uBCC0\uACBD \uC0AC\uD56D\uC740 \uC0AC\uC6A9\uC790 \uC2B9\uC778 \uD6C4\uC5D0\uB9CC \uC800\uC7A5\uB429\uB2C8\uB2E4. .hwpx \uD30C\uC77C \uC804\uC6A9\uC785\uB2C8\uB2E4.",
11831
- inputSchema: proposeFormObjectSchema,
11845
+ const buf = await out.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
11846
+ return { buffer: new Uint8Array(buf), results: allResults };
11847
+ }
11848
+ var proposeCellEditTool = {
11849
+ name: "propose_cell_edit",
11850
+ description: "HWPX \uBB38\uC11C\uC758 \uD45C \uC140 \uB0B4\uC6A9\uC744 XML \uC9C1\uC811 \uD328\uCE58 \uBC29\uC2DD\uC73C\uB85C \uC218\uC815\uD569\uB2C8\uB2E4. \uBCD1\uD569 \uC140(cellSpan/rowSpan)\uC774 \uC788\uB294 \uD45C\uC5D0\uC11C\uB3C4 \uBCD1\uD569 \uAD6C\uC870\uB97C \uC644\uC804\uD788 \uBCF4\uC874\uD569\uB2C8\uB2E4. \uBE48 \uC140(<hp:t/> self-closing \uB7F0)\uB3C4 \uCC44\uC6B8 \uC218 \uC788\uC5B4 \uC591\uC2DD(form) \uD3B8\uC9D1\uC5D0 \uC801\uD569\uD569\uB2C8\uB2E4. \uC140 \uC8FC\uC18C \uC9C0\uC815 \uBC29\uBC95: (1) \uC88C\uD45C \uBAA8\uB4DC \u2014 tableIndex + row + col \uC9C1\uC811 \uC9C0\uC815. (2) \uB808\uC774\uBE14 \uBAA8\uB4DC \u2014 label(\uC778\uC811 \uB808\uC774\uBE14 \uC140 \uD14D\uC2A4\uD2B8) + direction(right/below, \uAE30\uBCF8 right) + \uC120\uD0DD\uC801 tableIndex\uB85C \uB808\uC774\uBE14 \uC606/\uC544\uB798 \uC140\uC744 \uC790\uB3D9\uC73C\uB85C \uCC3E\uC544 \uD3B8\uC9D1. \uBCD1\uD569 \uB808\uC774\uBE14 \uC140\uB3C4 colSpan/rowSpan\uC744 \uBC18\uC601\uD558\uC5EC \uB300\uC0C1 \uC140\uC744 \uACC4\uC0B0\uD569\uB2C8\uB2E4. propose_edit/propose_form_fill\uC740 \uB9C8\uD06C\uB2E4\uC6B4 \uB77C\uC6B4\uB4DC\uD2B8\uB9BD\uC73C\uB85C \uBCD1\uD569 \uC140\uC744 \uC18C\uC2E4\uC2DC\uD0A4\uBBC0\uB85C, \uBCD1\uD569 \uC140\uC774 \uC788\uB294 \uD45C\uB97C \uC218\uC815\uD560 \uB54C\uB294 \uC774 \uD234\uC744 \uC0AC\uC6A9\uD558\uC138\uC694. .hwpx \uD30C\uC77C \uC804\uC6A9\uC785\uB2C8\uB2E4. .hwp\uB294 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC73C\uBA70 Hancom\uC5D0\uC11C .hwpx\uB85C \uC800\uC7A5 \uD6C4 \uC0AC\uC6A9\uD558\uC138\uC694. \uC218\uC815 \uC804\uC5D0 \uBC18\uB4DC\uC2DC read_document\uB85C \uC6D0\uBCF8\uC744 \uC77D\uACE0, expectedText\uB97C \uC9C0\uC815\uD558\uC5EC \uC548\uC804\uD558\uAC8C \uC218\uC815\uD558\uC138\uC694. \uBCC0\uACBD \uC0AC\uD56D\uC740 diff \uBBF8\uB9AC\uBCF4\uAE30\uC640 \uD568\uAED8 \uC0AC\uC6A9\uC790 \uC2B9\uC778\uC744 \uBC1B\uC740 \uD6C4\uC5D0\uB9CC \uC800\uC7A5\uB429\uB2C8\uB2E4.",
11851
+ inputSchema: proposeCellEditSchema,
11832
11852
  requiresApproval: true,
11833
11853
  propose: async ({
11834
11854
  input,
@@ -11836,145 +11856,196 @@ var proposeFormObjectTool = {
11836
11856
  }) => {
11837
11857
  const safePath = await resolveSafePath(ctx.cwd, input.path);
11838
11858
  const ext = extname3(safePath).toLowerCase();
11859
+ if (ext === ".hwp") {
11860
+ return "\uC624\uB958: propose_cell_edit\uC740 .hwpx \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. .hwp(\uAD6C\uD615 OLE \uBC14\uC774\uB108\uB9AC)\uB294 \uC9C1\uC811 \uD3B8\uC9D1\uC774 \uBD88\uAC00\uD569\uB2C8\uB2E4. \uD55C\uAE00 \uD504\uB85C\uADF8\uB7A8\uC5D0\uC11C '\uB2E4\uB978 \uC774\uB984\uC73C\uB85C \uC800\uC7A5 \u2192 .hwpx'\uB85C \uC800\uC7A5\uD55C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694. \uB610\uB294 propose_edit\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC788\uC73C\uB098, \uBCD1\uD569 \uC140\uC774 \uC18C\uC2E4\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.";
11861
+ }
11862
+ if (ext !== ".hwpx") {
11863
+ return `\uC624\uB958: propose_cell_edit\uC740 .hwpx \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. \uD604\uC7AC \uD30C\uC77C: ${ext}. \uD45C \uC140 \uC9C1\uC811 \uD3B8\uC9D1\uC740 .hwpx \uD3EC\uB9F7\uC5D0\uC11C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4.`;
11864
+ }
11839
11865
  let originalBuffer;
11840
11866
  try {
11841
11867
  originalBuffer = await readFile4(safePath);
11842
11868
  } catch {
11843
- return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}`;
11869
+ return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}. \uACBD\uB85C\uB97C \uD655\uC778\uD558\uAC70\uB098 read_document\uB85C \uBA3C\uC800 \uD655\uC778\uD558\uC138\uC694.`;
11844
11870
  }
11845
- const bufArray = new Uint8Array(originalBuffer.buffer);
11846
- const validationError = validateHwpxBuffer(ext, bufArray);
11847
- if (validationError) return validationError;
11848
- let zip;
11849
- try {
11850
- zip = await import_jszip.default.loadAsync(bufArray);
11851
- } catch {
11852
- return "\uC624\uB958: .hwpx(ZIP) \uD30C\uC77C\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD30C\uC77C\uC774 \uC190\uC0C1\uB418\uC5C8\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.";
11871
+ if (originalBuffer[0] !== 80 || originalBuffer[1] !== 75) {
11872
+ return "\uC624\uB958: \uD30C\uC77C\uC774 \uC720\uD6A8\uD55C .hwpx(ZIP) \uD3EC\uB9F7\uC774 \uC544\uB2D9\uB2C8\uB2E4. \uD30C\uC77C\uC774 \uC190\uC0C1\uB418\uC5C8\uAC70\uB098 \uAD6C\uD615 .hwp(OLE \uBC14\uC774\uB108\uB9AC) \uD3EC\uB9F7\uC785\uB2C8\uB2E4. \uD55C\uAE00 \uD504\uB85C\uADF8\uB7A8\uC5D0\uC11C .hwpx\uB85C \uC800\uC7A5 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.";
11853
11873
  }
11854
- const { objects, sectionFiles, sectionXmls } = await listFormObjectsFromZip(zip);
11855
- const resolvedEdits = [];
11856
- const resolveErrors = [];
11857
- for (let ei = 0; ei < input.edits.length; ei++) {
11858
- const edit = input.edits[ei];
11859
- if (!edit) continue;
11860
- const candidates = objects.filter((o) => o.name === edit.name);
11861
- if (candidates.length === 0) {
11862
- resolveErrors.push(
11863
- `\uD3B8\uC9D1 #${ei + 1}: name="${edit.name}"\uC778 \uC591\uC2DD \uAC1C\uCCB4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. list_form_objects\uB85C \uBB38\uC11C\uC758 \uC591\uC2DD \uAC1C\uCCB4 \uBAA9\uB85D\uC744 \uD655\uC778\uD558\uC138\uC694.`
11864
- );
11865
- continue;
11874
+ const zipForLabel = await import_jszip2.default.loadAsync(new Uint8Array(originalBuffer.buffer));
11875
+ const sectionFilesForLabel = Object.keys(zipForLabel.files).filter((name) => /^Contents\/section\d+\.xml$/.test(name)).sort();
11876
+ const sectionInfos = [];
11877
+ let globalTblOffset = 0;
11878
+ for (const sf of sectionFilesForLabel) {
11879
+ const entry = zipForLabel.file(sf);
11880
+ const xml = entry ? await entry.async("string") : "";
11881
+ const tokens = tokenizeHwpxXml(xml);
11882
+ let count = 0;
11883
+ let d = 0;
11884
+ for (const tok of tokens) {
11885
+ if (tok.kind === "tbl_open") {
11886
+ if (d === 0) count++;
11887
+ d++;
11888
+ } else if (tok.kind === "tbl_close") {
11889
+ d--;
11890
+ }
11866
11891
  }
11867
- let target;
11868
- if (candidates.length > 1) {
11869
- if (edit.index === void 0) {
11870
- const indices = candidates.map((c) => c.index).join(", ");
11871
- resolveErrors.push(
11872
- `\uD3B8\uC9D1 #${ei + 1}: name="${edit.name}"\uC778 \uC591\uC2DD \uAC1C\uCCB4\uAC00 ${candidates.length}\uAC1C \uBC1C\uACAC\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (\uBB38\uC11C \uC778\uB371\uC2A4: ${indices}). index \uD544\uB4DC\uB85C \uB300\uC0C1\uC744 \uC9C0\uC815\uD558\uC138\uC694.`
11873
- );
11874
- continue;
11892
+ sectionInfos.push({ xml, tblCount: count, globalOffset: globalTblOffset });
11893
+ globalTblOffset += count;
11894
+ }
11895
+ function resolveLabelAcrossSections(label, direction, scopedTableIndex) {
11896
+ const trimmedLabel = label.trim();
11897
+ const allMatches = [];
11898
+ for (const si of sectionInfos) {
11899
+ const tokens = tokenizeHwpxXml(si.xml);
11900
+ let localStart = 0;
11901
+ let localEnd = si.tblCount - 1;
11902
+ if (scopedTableIndex !== void 0) {
11903
+ const localIdx2 = scopedTableIndex - si.globalOffset;
11904
+ if (localIdx2 < 0 || localIdx2 >= si.tblCount) continue;
11905
+ localStart = localIdx2;
11906
+ localEnd = localIdx2;
11875
11907
  }
11876
- const byIndex = candidates.find((c) => c.index === edit.index);
11877
- if (!byIndex) {
11878
- resolveErrors.push(
11879
- `\uD3B8\uC9D1 #${ei + 1}: name="${edit.name}", index=${edit.index}\uC778 \uC591\uC2DD \uAC1C\uCCB4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
11880
- );
11881
- continue;
11908
+ for (let li = localStart; li <= localEnd; li++) {
11909
+ const tblRange = findTopLevelTableRange(tokens, li);
11910
+ if (!tblRange) continue;
11911
+ const directTcs = collectDirectTcRanges(tokens, tblRange.start, tblRange.end);
11912
+ for (const tc of directTcs) {
11913
+ const cellText = readOwnTextFromTc(si.xml, tokens, tc.start, tc.end).trim();
11914
+ if (cellText === trimmedLabel) {
11915
+ const addrSpan2 = readCellAddrSpan(tokens, tc.start, tc.end);
11916
+ if (addrSpan2) {
11917
+ allMatches.push({
11918
+ globalTableIndex: si.globalOffset + li,
11919
+ addrSpan: addrSpan2,
11920
+ sectionXml: si.xml,
11921
+ sectionOffset: si.globalOffset
11922
+ });
11923
+ }
11924
+ }
11925
+ }
11882
11926
  }
11883
- target = byIndex;
11927
+ }
11928
+ if (allMatches.length === 0) {
11929
+ const scope = scopedTableIndex !== void 0 ? `\uD45C ${scopedTableIndex}` : "\uBB38\uC11C \uB0B4 \uBAA8\uB4E0 \uD45C";
11930
+ return {
11931
+ error: `\uB808\uC774\uBE14 "${label}"\uC744(\uB97C) ${scope}\uC5D0\uC11C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. read_document\uB85C \uD45C \uB0B4\uC6A9\uC744 \uD655\uC778\uD558\uC138\uC694.`
11932
+ };
11933
+ }
11934
+ if (allMatches.length > 1) {
11935
+ const locs = allMatches.map(
11936
+ (m) => `\uD45C ${m.globalTableIndex} (\uD589 ${m.addrSpan.rowAddr}, \uC5F4 ${m.addrSpan.colAddr})`
11937
+ ).join(", ");
11938
+ return {
11939
+ error: `\uB808\uC774\uBE14 "${label}"\uC774(\uAC00) \uC5EC\uB7EC \uC140\uC5D0\uC11C \uBC1C\uACAC\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${locs}. tableIndex\uB85C \uD0D0\uC0C9 \uBC94\uC704\uB97C \uC881\uD788\uAC70\uB098 \uC88C\uD45C(row/col)\uB97C \uC9C1\uC811 \uC9C0\uC815\uD558\uC138\uC694.`
11940
+ };
11941
+ }
11942
+ const match = allMatches[0];
11943
+ const { addrSpan, globalTableIndex, sectionXml, sectionOffset } = match;
11944
+ let targetRow;
11945
+ let targetCol;
11946
+ if (direction === "right") {
11947
+ targetRow = addrSpan.rowAddr;
11948
+ targetCol = addrSpan.colAddr + addrSpan.colSpan;
11884
11949
  } else {
11885
- target = candidates[0];
11886
- if (edit.index !== void 0 && edit.index !== target.index) {
11887
- resolveErrors.push(
11888
- `\uD3B8\uC9D1 #${ei + 1}: name="${edit.name}"\uC758 \uBB38\uC11C \uC778\uB371\uC2A4\uB294 ${target.index}\uC774\uC9C0\uB9CC index=${edit.index}\uAC00 \uC9C0\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`
11889
- );
11890
- continue;
11950
+ targetRow = addrSpan.rowAddr + addrSpan.rowSpan;
11951
+ targetCol = addrSpan.colAddr;
11952
+ }
11953
+ const localIdx = globalTableIndex - sectionOffset;
11954
+ const tokens2 = tokenizeHwpxXml(sectionXml);
11955
+ const tblRange2 = findTopLevelTableRange(tokens2, localIdx);
11956
+ if (!tblRange2) {
11957
+ return { error: `\uD45C ${globalTableIndex}\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uB0B4\uBD80 \uC624\uB958).` };
11958
+ }
11959
+ const directTcs2 = collectDirectTcRanges(tokens2, tblRange2.start, tblRange2.end);
11960
+ const tblTokens2 = tokens2.filter((t) => t.pos >= tblRange2.start && t.pos < tblRange2.end);
11961
+ let targetExists = false;
11962
+ for (const tc of directTcs2) {
11963
+ const tcTokens = tblTokens2.filter((t) => t.pos >= tc.start && t.pos < tc.end);
11964
+ if (tcTokens.some(
11965
+ (t) => t.kind === "cell_addr" && t.colAddr === targetCol && t.rowAddr === targetRow
11966
+ )) {
11967
+ targetExists = true;
11968
+ break;
11891
11969
  }
11892
11970
  }
11893
- resolvedEdits.push({
11894
- target,
11895
- set: edit.set,
11896
- expected: edit.expected,
11897
- editIdx: ei
11898
- });
11971
+ if (!targetExists) {
11972
+ const dirLabel = direction === "right" ? "\uC624\uB978\uCABD" : "\uC544\uB798";
11973
+ return {
11974
+ error: `\uB808\uC774\uBE14 "${label}" (\uD45C ${globalTableIndex}, \uD589 ${addrSpan.rowAddr}, \uC5F4 ${addrSpan.colAddr})\uC758 ${dirLabel} \uC140 (\uD589 ${targetRow}, \uC5F4 ${targetCol})\uC774 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. direction \uB610\uB294 \uC88C\uD45C\uB97C \uD655\uC778\uD558\uC138\uC694.`
11975
+ };
11976
+ }
11977
+ return { tableIndex: globalTableIndex, row: targetRow, col: targetCol };
11978
+ }
11979
+ const resolvedEdits = [];
11980
+ const resolveErrors = [];
11981
+ for (let i = 0; i < input.edits.length; i++) {
11982
+ const e = input.edits[i];
11983
+ if (!e) continue;
11984
+ if (e.label !== void 0 && (e.row !== void 0 || e.col !== void 0)) {
11985
+ resolveErrors.push(
11986
+ `\uD3B8\uC9D1 #${i + 1}: label\uACFC row/col\uC744 \uB3D9\uC2DC\uC5D0 \uC9C0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC88C\uD45C \uBAA8\uB4DC(tableIndex+row+col) \uB610\uB294 \uB808\uC774\uBE14 \uBAA8\uB4DC(label) \uC911 \uD558\uB098\uB9CC \uC0AC\uC6A9\uD558\uC138\uC694.`
11987
+ );
11988
+ } else if (e.label !== void 0) {
11989
+ const direction = e.direction ?? "right";
11990
+ const resolved = resolveLabelAcrossSections(e.label, direction, e.tableIndex);
11991
+ if ("error" in resolved) {
11992
+ resolveErrors.push(`\uD3B8\uC9D1 #${i + 1} (\uB808\uC774\uBE14 "${e.label}"): ${resolved.error}`);
11993
+ } else {
11994
+ resolvedEdits.push({
11995
+ tableIndex: resolved.tableIndex,
11996
+ row: resolved.row,
11997
+ col: resolved.col,
11998
+ newText: e.newText,
11999
+ expectedText: e.expectedText,
12000
+ label: e.label
12001
+ });
12002
+ }
12003
+ } else if (e.tableIndex !== void 0 && e.row !== void 0 && e.col !== void 0) {
12004
+ resolvedEdits.push({
12005
+ tableIndex: e.tableIndex,
12006
+ row: e.row,
12007
+ col: e.col,
12008
+ newText: e.newText,
12009
+ expectedText: e.expectedText
12010
+ });
12011
+ } else {
12012
+ resolveErrors.push(
12013
+ `\uD3B8\uC9D1 #${i + 1}: \uC88C\uD45C \uBAA8\uB4DC(tableIndex+row+col) \uB610\uB294 \uB808\uC774\uBE14 \uBAA8\uB4DC(label) \uC911 \uD558\uB098\uB97C \uC644\uC804\uD788 \uC9C0\uC815\uD558\uC138\uC694. read_document\uB85C \uD45C \uB0B4\uC6A9\uC744 \uBA3C\uC800 \uD655\uC778\uD558\uC138\uC694.`
12014
+ );
12015
+ }
11899
12016
  }
11900
12017
  if (resolveErrors.length > 0) {
11901
- return `\uC624\uB958: \uB2E4\uC74C \uD3B8\uC9D1 \uB300\uC0C1\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC5B4 \uD30C\uC77C\uC744 \uC218\uC815\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
12018
+ return `\uC624\uB958: \uB2E4\uC74C \uB808\uC774\uBE14\uC744 \uD574\uC11D\uD560 \uC218 \uC5C6\uC5B4 \uD30C\uC77C\uC744 \uC218\uC815\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
11902
12019
  ${resolveErrors.join("\n")}`;
11903
12020
  }
11904
- const sectionEditMap = /* @__PURE__ */ new Map();
11905
- for (const re of resolvedEdits) {
11906
- const si = sectionFiles.indexOf(re.target.sectionFile);
11907
- if (si < 0) continue;
11908
- if (!sectionEditMap.has(si)) sectionEditMap.set(si, []);
11909
- sectionEditMap.get(si).push({
11910
- target: re.target,
11911
- set: re.set,
11912
- expected: re.expected
12021
+ const editRequests = resolvedEdits.map((e) => ({
12022
+ tableIndex: e.tableIndex,
12023
+ row: e.row,
12024
+ col: e.col,
12025
+ newText: e.newText,
12026
+ expectedText: e.expectedText
12027
+ }));
12028
+ const { buffer: newBuffer, results } = await applyEditsToHwpx(
12029
+ new Uint8Array(originalBuffer.buffer),
12030
+ editRequests
12031
+ );
12032
+ const failedResults = results.map((r, i) => ({ r, i })).filter(({ r }) => !r.success);
12033
+ if (failedResults.length > 0) {
12034
+ const messages = failedResults.map(({ r, i }) => {
12035
+ const e = resolvedEdits[i];
12036
+ const label = e?.label ? `\uB808\uC774\uBE14 "${e.label}" \u2192 ` : "";
12037
+ return `\uD3B8\uC9D1 #${i + 1} (${label}\uD45C ${e?.tableIndex ?? "?"}, \uD589 ${e?.row ?? "?"}, \uC5F4 ${e?.col ?? "?"}): ${r.error}`;
11913
12038
  });
11914
- }
11915
- const sectionResults = [];
11916
- const newSectionXmls = [...sectionXmls];
11917
- for (const [si, edits] of sectionEditMap) {
11918
- const xml = sectionXmls[si] ?? "";
11919
- const { newXml, results } = applyFormObjectEdits(xml, edits);
11920
- newSectionXmls[si] = newXml;
11921
- sectionResults.push({ si, results, edits });
11922
- }
11923
- const failMessages = [];
11924
- const successMap = /* @__PURE__ */ new Map();
11925
- for (const sr of sectionResults) {
11926
- for (let i = 0; i < sr.edits.length; i++) {
11927
- const editReq = sr.edits[i];
11928
- const result = sr.results[i];
11929
- const resolved = resolvedEdits.find((r) => r.target === editReq.target);
11930
- if (resolved) {
11931
- successMap.set(resolved.editIdx, result);
11932
- if (!result.success) {
11933
- failMessages.push(
11934
- `\uD3B8\uC9D1 #${resolved.editIdx + 1} (name="${editReq.target.name}"): ${result.error}`
11935
- );
11936
- }
11937
- }
11938
- }
11939
- }
11940
- if (failMessages.length > 0) {
11941
12039
  return `\uC624\uB958: \uB2E4\uC74C \uD3B8\uC9D1\uC744 \uC801\uC6A9\uD560 \uC218 \uC5C6\uC5B4 \uD30C\uC77C\uC744 \uC218\uC815\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
11942
- ${failMessages.join("\n")}`;
11943
- }
11944
- const out = new import_jszip.default();
11945
- const mimetypeEntry = zip.file("mimetype");
11946
- if (mimetypeEntry) {
11947
- out.file("mimetype", await mimetypeEntry.async("uint8array"), { compression: "STORE" });
11948
- }
11949
- for (const [name, entry] of Object.entries(zip.files)) {
11950
- if (name === "mimetype" || entry.dir) continue;
11951
- const si = sectionFiles.indexOf(name);
11952
- if (si >= 0) {
11953
- out.file(name, newSectionXmls[si] ?? "");
11954
- } else {
11955
- out.file(name, await entry.async("uint8array"));
11956
- }
12040
+ ${messages.join("\n")}`;
11957
12041
  }
11958
- const buf = await out.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
11959
- const newBuffer = new Uint8Array(buf);
11960
- const typeKo = {
11961
- button: "PushButton",
11962
- checkBox: "CheckBox",
11963
- radioButton: "RadioButton",
11964
- comboBox: "ComboBox",
11965
- edit: "Edit"
11966
- };
11967
- const diffLines = ["| \uC591\uC2DD \uAC1C\uCCB4 | \uC774\uC804 | \uC774\uD6C4 |", "| --- | --- | --- |"];
11968
- for (const re of resolvedEdits) {
11969
- const result = successMap.get(re.editIdx);
11970
- const oldVal = result?.oldValue ?? "";
11971
- const oldStr = typeof oldVal === "boolean" ? oldVal ? "CHECKED" : "UNCHECKED" : String(oldVal);
11972
- let newStr;
11973
- if (re.set.caption !== void 0) newStr = re.set.caption;
11974
- else if (re.set.checked !== void 0) newStr = re.set.checked ? "CHECKED" : "UNCHECKED";
11975
- else if (re.set.selected !== void 0) newStr = re.set.selected;
11976
- else newStr = re.set.text ?? "";
11977
- diffLines.push(`| ${re.target.name}(${typeKo[re.target.type]}) | ${oldStr} | ${newStr} |`);
12042
+ const diffLines = ["| \uD45C\xB7\uC140 | \uC774\uC804 | \uC774\uD6C4 |", "| --- | --- | --- |"];
12043
+ for (let i = 0; i < resolvedEdits.length; i++) {
12044
+ const e = resolvedEdits[i];
12045
+ if (!e) continue;
12046
+ const oldText = results[i]?.oldText ?? "";
12047
+ const addr = e.label ? `\uB808\uC774\uBE14 "${e.label}" \u2192 #${e.tableIndex} (${e.row},${e.col})` : `#${e.tableIndex} (${e.row},${e.col})`;
12048
+ diffLines.push(`| ${addr} | ${oldText} | ${e.newText} |`);
11978
12049
  }
11979
12050
  const diff = diffLines.join("\n");
11980
12051
  const { outputPath, willConvertFormat } = resolveOutputPath(safePath);
@@ -11983,7 +12054,7 @@ ${failMessages.join("\n")}`;
11983
12054
  return {
11984
12055
  proposal: {
11985
12056
  id: proposalId,
11986
- kind: "form-object",
12057
+ kind: "cell-edit",
11987
12058
  targetPath: outputPath,
11988
12059
  stagedPath,
11989
12060
  summary: input.summary,
@@ -12000,640 +12071,790 @@ ${failMessages.join("\n")}`;
12000
12071
  };
12001
12072
  }
12002
12073
  };
12003
- var DOC_EXTENSIONS = /* @__PURE__ */ new Set([
12004
- ".hwp",
12005
- ".hwpx",
12006
- ".hwpml",
12007
- ".docx",
12008
- ".doc",
12009
- ".xlsx",
12010
- ".xls",
12011
- ".pdf",
12012
- ".pptx",
12013
- ".ppt",
12014
- ".md",
12015
- ".txt"
12016
- ]);
12017
- var SKIP_DIRS = /* @__PURE__ */ new Set([
12018
- "node_modules",
12019
- ".git",
12020
- ".DS_Store",
12021
- "__pycache__",
12022
- "dist",
12023
- "build",
12024
- ".next"
12025
- ]);
12026
- var MAX_DEPTH = 4;
12027
- var MAX_FILES = 500;
12028
- var listFilesSchema = z4.object({
12029
- dir: z4.string().optional().describe("\uBAA9\uB85D\uC744 \uC870\uD68C\uD560 \uB514\uB809\uD130\uB9AC (\uBBF8\uC9C0\uC815 \uC2DC cwd \uC804\uCCB4)")
12074
+ var findInDocumentSchema = z4.object({
12075
+ path: z4.string().describe("\uAC80\uC0C9\uD560 .hwpx \uBB38\uC11C \uACBD\uB85C"),
12076
+ query: z4.string().min(1).describe("\uCC3E\uC744 \uD14D\uC2A4\uD2B8"),
12077
+ caseSensitive: z4.boolean().optional().describe("\uB300\uC18C\uBB38\uC790 \uAD6C\uBD84 (\uAE30\uBCF8 false)")
12030
12078
  });
12031
- async function collectFiles(dir, cwd, depth, entries) {
12032
- if (depth > MAX_DEPTH || entries.length >= MAX_FILES) return;
12033
- let items;
12034
- try {
12035
- items = await readdir3(dir);
12036
- } catch {
12037
- return;
12038
- }
12039
- for (const item of items) {
12040
- if (entries.length >= MAX_FILES) break;
12041
- if (item.startsWith(".")) continue;
12042
- if (SKIP_DIRS.has(item)) continue;
12043
- const fullPath = join4(dir, item);
12044
- let info;
12045
- try {
12046
- info = await stat3(fullPath);
12047
- } catch {
12048
- continue;
12079
+ function unescapeXml(text3) {
12080
+ return text3.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
12081
+ }
12082
+ function windowText(text3, query, caseSensitive, maxLen = 60) {
12083
+ const compare3 = caseSensitive ? text3 : text3.toLowerCase();
12084
+ const compareQuery = caseSensitive ? query : query.toLowerCase();
12085
+ const idx = compare3.indexOf(compareQuery);
12086
+ if (idx < 0) return text3.slice(0, maxLen);
12087
+ const half = Math.floor((maxLen - query.length) / 2);
12088
+ const start = Math.max(0, idx - half);
12089
+ const end = Math.min(text3.length, idx + query.length + half);
12090
+ const prefix = start > 0 ? "\u2026" : "";
12091
+ const suffix = end < text3.length ? "\u2026" : "";
12092
+ return prefix + text3.slice(start, end) + suffix;
12093
+ }
12094
+ function findInSectionXmls(xmls, query, caseSensitive) {
12095
+ const hits = [];
12096
+ let globalOffset = 0;
12097
+ for (let si = 0; si < xmls.length; si++) {
12098
+ const xml = xmls[si] ?? "";
12099
+ const tokens = tokenizeHwpxXml(xml);
12100
+ let localCount = 0;
12101
+ {
12102
+ let depth = 0;
12103
+ for (const tok of tokens) {
12104
+ if (tok.kind === "tbl_open") {
12105
+ if (depth === 0) localCount++;
12106
+ depth++;
12107
+ } else if (tok.kind === "tbl_close") {
12108
+ depth--;
12109
+ }
12110
+ }
12111
+ }
12112
+ for (let ti = 0; ti < localCount; ti++) {
12113
+ const range = findTopLevelTableRange(tokens, ti);
12114
+ if (!range) continue;
12115
+ const cells = collectDirectTcRanges(tokens, range.start, range.end);
12116
+ const tblTokens = tokens.filter((t) => t.pos >= range.start && t.pos < range.end);
12117
+ for (const cell of cells) {
12118
+ const cellText = readOwnTextFromTc(xml, tokens, cell.start, cell.end);
12119
+ const compare3 = caseSensitive ? cellText : cellText.toLowerCase();
12120
+ const compareQuery = caseSensitive ? query : query.toLowerCase();
12121
+ if (!compare3.includes(compareQuery)) continue;
12122
+ const cellTokens = tblTokens.filter((t) => t.pos >= cell.start && t.pos < cell.end);
12123
+ const addrTok = cellTokens.find((t) => t.kind === "cell_addr");
12124
+ if (addrTok === void 0) continue;
12125
+ const row = addrTok.rowAddr ?? 0;
12126
+ const col = addrTok.colAddr ?? 0;
12127
+ const truncated = cellText.length > 60 ? `${cellText.slice(0, 57)}\u2026` : cellText;
12128
+ hits.push({
12129
+ kind: "\uD45C",
12130
+ tableIndex: globalOffset + ti,
12131
+ row,
12132
+ col,
12133
+ text: truncated
12134
+ });
12135
+ }
12049
12136
  }
12050
- const relPath = relative2(cwd, fullPath);
12051
- const ext = extname4(item).toLowerCase();
12052
- const isDoc = DOC_EXTENSIONS.has(ext);
12053
- if (info.isDirectory()) {
12054
- entries.push({ path: relPath + "/", isDir: true, isDoc: false });
12055
- await collectFiles(fullPath, cwd, depth + 1, entries);
12056
- } else if (info.isFile()) {
12057
- entries.push({ path: relPath, isDir: false, isDoc });
12137
+ {
12138
+ const bodySegments = [];
12139
+ let tblDepth = 0;
12140
+ let segStart = 0;
12141
+ for (const tok of tokens) {
12142
+ if (tok.kind === "tbl_open") {
12143
+ if (tblDepth === 0) {
12144
+ if (tok.pos > segStart) {
12145
+ bodySegments.push({ start: segStart, end: tok.pos });
12146
+ }
12147
+ }
12148
+ tblDepth++;
12149
+ } else if (tok.kind === "tbl_close") {
12150
+ tblDepth--;
12151
+ if (tblDepth === 0) {
12152
+ segStart = tok.end;
12153
+ }
12154
+ }
12155
+ }
12156
+ if (segStart < xml.length) {
12157
+ bodySegments.push({ start: segStart, end: xml.length });
12158
+ }
12159
+ const tRe = /<hp:t>([\s\S]*?)<\/hp:t>/g;
12160
+ for (const seg of bodySegments) {
12161
+ const slice = xml.slice(seg.start, seg.end);
12162
+ tRe.lastIndex = 0;
12163
+ let m = tRe.exec(slice);
12164
+ while (m !== null) {
12165
+ const rawText = m[1] ?? "";
12166
+ const decodedText = unescapeXml(rawText);
12167
+ const compare3 = caseSensitive ? decodedText : decodedText.toLowerCase();
12168
+ const compareQuery = caseSensitive ? query : query.toLowerCase();
12169
+ if (compare3.includes(compareQuery)) {
12170
+ hits.push({
12171
+ kind: "\uBCF8\uBB38",
12172
+ section: si,
12173
+ text: windowText(decodedText, query, caseSensitive)
12174
+ });
12175
+ }
12176
+ m = tRe.exec(slice);
12177
+ }
12178
+ }
12058
12179
  }
12180
+ globalOffset += localCount;
12059
12181
  }
12182
+ return hits;
12060
12183
  }
12061
- var listFilesTool = {
12062
- name: "list_files",
12063
- description: "\uD604\uC7AC \uC791\uC5C5 \uB514\uB809\uD130\uB9AC(cwd) \uC774\uD558\uC758 \uD30C\uC77C \uBAA9\uB85D\uC744 \uBC18\uD658\uD569\uB2C8\uB2E4. \uBB38\uC11C \uD30C\uC77C(.hwp/.hwpx/.docx/.xlsx/.pdf \uB4F1)\uC774 \uCD5C\uC0C1\uB2E8\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4. \uAE4A\uC774\uB294 \uCD5C\uB300 4\uB2E8\uACC4\uAE4C\uC9C0\uC785\uB2C8\uB2E4.",
12064
- inputSchema: listFilesSchema,
12184
+ var MAX_HITS = 50;
12185
+ function formatHits(hits, query) {
12186
+ const shown = hits.slice(0, MAX_HITS);
12187
+ const lines = shown.map((h) => {
12188
+ if (h.kind === "\uD45C") {
12189
+ return `[\uD45C] tableIndex=${h.tableIndex}, row=${h.row}, col=${h.col} \u2014 "${h.text}" (propose_cell_edit\uC73C\uB85C \uC218\uC815 \uAC00\uB2A5)`;
12190
+ }
12191
+ return `[\uBCF8\uBB38] \uC139\uC158 ${h.section} \u2014 "\u2026${h.text}\u2026" (propose_find_replace\uB85C \uC218\uC815 \uAC00\uB2A5)`;
12192
+ });
12193
+ const notice = hits.length > MAX_HITS ? `
12194
+ (\uCD1D ${hits.length}\uAC1C \uB9E4\uCE6D \uC911 ${MAX_HITS}\uAC1C\uB9CC \uD45C\uC2DC\uB429\uB2C8\uB2E4.)` : "";
12195
+ return `"${query}" \uAC80\uC0C9 \uACB0\uACFC: ${hits.length}\uAC1C
12196
+
12197
+ ${lines.join("\n")}${notice}`;
12198
+ }
12199
+ var findInDocumentTool = {
12200
+ name: "find_in_document",
12201
+ description: "\uBB38\uC11C\uC5D0\uC11C \uD2B9\uC815 \uD14D\uC2A4\uD2B8\uAC00 **\uC5B4\uB514\uC5D0 \uC788\uB294\uC9C0** \uC704\uCE58\uB97C \uCC3E\uC544 \uC90D\uB2C8\uB2E4. \uD45C \uC548\uC758 \uC140\uC774\uBA74 propose_cell_edit\uC5D0\uC11C \uBC14\uB85C \uC4F8 \uC218 \uC788\uB294 tableIndex\xB7row\xB7col \uC88C\uD45C\uB97C, \uBCF8\uBB38\uC774\uBA74 \uC8FC\uBCC0 \uB9E5\uB77D\uC744 \uBC18\uD658\uD569\uB2C8\uB2E4. \uD070 \uBB38\uC11C\uC5D0\uC11C \uC804\uCCB4\uB97C \uC77D\uC9C0 \uC54A\uACE0 \uC218\uC815 \uB300\uC0C1\uC744 \uC815\uD655\uD788 \uC9C0\uC815\uD560 \uB54C \uC0AC\uC6A9\uD558\uC138\uC694. .hwpx \uC804\uC6A9.",
12202
+ inputSchema: findInDocumentSchema,
12065
12203
  requiresApproval: false,
12066
12204
  execute: async ({
12067
12205
  input,
12068
12206
  ctx
12069
12207
  }) => {
12070
- const targetDir = input.dir ? await resolveSafePath(ctx.cwd, input.dir) : ctx.cwd;
12071
- const entries = [];
12072
- await collectFiles(targetDir, ctx.cwd, 0, entries);
12073
- if (entries.length === 0) {
12074
- return "\uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.";
12208
+ let safePath;
12209
+ try {
12210
+ safePath = await resolveSafePath(ctx.cwd, input.path);
12211
+ } catch (err) {
12212
+ return `\uC624\uB958: \uACBD\uB85C\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}. ${String(err)}`;
12075
12213
  }
12076
- const docs = entries.filter((e) => e.isDoc);
12077
- const dirs = entries.filter((e) => e.isDir);
12078
- const others = entries.filter((e) => !e.isDoc && !e.isDir);
12079
- const allSorted = [...docs, ...dirs, ...others];
12080
- const lines = allSorted.map((e) => {
12081
- const icon = e.isDir ? "\u{1F4C1}" : e.isDoc ? "\u{1F4C4}" : " ";
12082
- return `${icon} ${e.path}`;
12083
- });
12084
- const truncateNotice = entries.length >= MAX_FILES ? `
12085
- (\uCD5C\uB300 ${MAX_FILES}\uAC1C\uAE4C\uC9C0 \uD45C\uC2DC\uB429\uB2C8\uB2E4. \uB354 \uC881\uC740 \uBC94\uC704\uB97C \uC9C0\uC815\uD558\uC138\uC694.)` : "";
12086
- return lines.join("\n") + truncateNotice;
12087
- }
12088
- };
12089
- var cellEditItemSchema = z5.object({
12090
- tableIndex: z5.number().int().nonnegative().optional().describe(
12091
- "\uC88C\uD45C \uBAA8\uB4DC: 0-based \uD45C \uC778\uB371\uC2A4(read_document\uC758 kordoc \uBE14\uB85D \uC21C\uC11C, \uC911\uCCA9\uD45C \uC81C\uC678). \uB808\uC774\uBE14 \uBAA8\uB4DC\uC5D0\uC120 \uD0D0\uC0C9 \uBC94\uC704 \uC81C\uD55C\uC6A9(\uC120\uD0DD)."
12092
- ),
12093
- row: z5.number().int().nonnegative().optional().describe("\uC88C\uD45C \uBAA8\uB4DC: \uC140\uC758 rowAddr (0-based)"),
12094
- col: z5.number().int().nonnegative().optional().describe("\uC88C\uD45C \uBAA8\uB4DC: \uC140\uC758 colAddr (0-based)"),
12095
- label: z5.string().optional().describe(
12096
- "\uB808\uC774\uBE14 \uBAA8\uB4DC: \uAE30\uC900 \uC140 \uD14D\uC2A4\uD2B8(\uD2B8\uB9BC \uBE44\uAD50). \uC774 \uC140\uC758 direction \uBC29\uD5A5 \uC778\uC811 \uC140\uC5D0 newText\uB97C \uAE30\uB85D. \uC88C\uD45C \uBAA8\uB4DC\uBA74 \uC0DD\uB7B5."
12097
- ),
12098
- direction: z5.enum(["right", "below"]).optional().describe("\uB808\uC774\uBE14 \uBAA8\uB4DC \uBC29\uD5A5. \uAE30\uBCF8 right(\uC624\uB978\uCABD \uC140), below(\uC544\uB798 \uC140). \uBCD1\uD569 span \uACE0\uB824."),
12099
- newText: z5.string().describe("\uC140\uC5D0 \uC4F8 \uC0C8 \uD14D\uC2A4\uD2B8"),
12100
- expectedText: z5.string().optional().describe(
12101
- "\uD604\uC7AC \uC140 \uD14D\uC2A4\uD2B8(\uC548\uC804 \uAC80\uC99D\uC6A9). \uBD88\uC77C\uCE58 \uC2DC \uC218\uC815\uD558\uC9C0 \uC54A\uC74C. \uC798\uBABB\uB41C \uC140 \uC218\uC815 \uBC29\uC9C0\uB97C \uC704\uD574 \uAD8C\uC7A5."
12102
- )
12103
- }).describe(
12104
- "\uD3B8\uC9D1 \uD56D\uBAA9. \uC88C\uD45C \uBAA8\uB4DC(tableIndex+row+col) \uB610\uB294 \uB808\uC774\uBE14 \uBAA8\uB4DC(label[+direction]) \uC911 \uD558\uB098\uB97C \uC0AC\uC6A9\uD558\uC138\uC694. \uB458 \uB2E4 \uC9C0\uC815\uD558\uAC70\uB098 \uB458 \uB2E4 \uC0DD\uB7B5\uD558\uBA74 \uC624\uB958\uC785\uB2C8\uB2E4."
12105
- );
12106
- var proposeCellEditSchema = z5.object({
12107
- path: z5.string().describe("\uC218\uC815\uD560 .hwpx \uD30C\uC77C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
12108
- edits: z5.array(cellEditItemSchema).min(1).describe(
12109
- "\uD3B8\uC9D1 \uBAA9\uB85D. \uAC01 \uD56D\uBAA9\uC740 \uC88C\uD45C \uBAA8\uB4DC(tableIndex+row+col) \uB610\uB294 \uB808\uC774\uBE14 \uBAA8\uB4DC(label+direction) \uC911 \uD558\uB098"
12110
- ),
12111
- summary: z5.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
12112
- });
12113
- function tokenizeHwpxXml(xml) {
12114
- const tokens = [];
12115
- const re = /<hp:tbl[\s>]|<\/hp:tbl>|<hp:tc[\s>]|<\/hp:tc>|<hp:t\/>|<hp:t>|<hp:cellAddr[^/>]*|<hp:cellSpan[^/>]*/g;
12116
- let startPos = 0;
12117
- let m = re.exec(xml);
12118
- while (m !== null) {
12119
- const raw = m[0];
12120
- const pos = m.index;
12121
- if (raw.startsWith("<hp:tbl")) {
12122
- tokens.push({ kind: "tbl_open", pos, end: pos + raw.length });
12123
- } else if (raw === "</hp:tbl>") {
12124
- tokens.push({ kind: "tbl_close", pos, end: pos + raw.length });
12125
- } else if (raw.startsWith("<hp:tc")) {
12126
- tokens.push({ kind: "tc_open", pos, end: pos + raw.length });
12127
- } else if (raw === "</hp:tc>") {
12128
- tokens.push({ kind: "tc_close", pos, end: pos + raw.length });
12129
- } else if (raw === "<hp:t/>") {
12130
- tokens.push({ kind: "t_empty", pos, end: pos + raw.length });
12131
- } else if (raw === "<hp:t>") {
12132
- tokens.push({ kind: "t_open", pos, end: pos + raw.length });
12133
- } else if (raw.startsWith("<hp:cellAddr")) {
12134
- const colM = raw.match(/colAddr="(\d+)"/);
12135
- const rowM = raw.match(/rowAddr="(\d+)"/);
12136
- if (colM && rowM) {
12137
- const selfClose = xml.indexOf("/>", pos);
12138
- const end = selfClose >= 0 ? selfClose + 2 : pos + raw.length;
12139
- tokens.push({
12140
- kind: "cell_addr",
12141
- pos,
12142
- end,
12143
- colAddr: Number(colM[1]),
12144
- rowAddr: Number(rowM[1])
12145
- });
12214
+ const ext = extname4(safePath).toLowerCase();
12215
+ if (ext !== ".hwpx") {
12216
+ return "find_in_document\uB294 .hwpx \uC804\uC6A9\uC785\uB2C8\uB2E4. \uBCF8\uBB38 \uD14D\uC2A4\uD2B8 \uAC80\uC0C9\uC740 read_document\uC758 search \uBAA8\uB4DC\uB97C \uC0AC\uC6A9\uD558\uC138\uC694. (.hwp \uD30C\uC77C\uC740 \uD55C\uAE00\uC5D0\uC11C \uB2E4\uB978 \uC774\uB984\uC73C\uB85C \uC800\uC7A5 \u2192 .hwpx\uB85C \uBCC0\uD658 \uD6C4 \uC0AC\uC6A9\uD558\uC138\uC694.)";
12217
+ }
12218
+ let bytes;
12219
+ try {
12220
+ bytes = await readFile5(safePath);
12221
+ } catch {
12222
+ return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}. \uACBD\uB85C\uB97C \uD655\uC778\uD558\uC138\uC694.`;
12223
+ }
12224
+ if (bytes[0] !== 80 || bytes[1] !== 75) {
12225
+ return "\uC624\uB958: \uD30C\uC77C\uC774 \uC720\uD6A8\uD55C .hwpx(ZIP) \uD3EC\uB9F7\uC774 \uC544\uB2D9\uB2C8\uB2E4. \uD30C\uC77C\uC774 \uC190\uC0C1\uB418\uC5C8\uAC70\uB098 \uAD6C\uD615 .hwp(OLE \uBC14\uC774\uB108\uB9AC) \uD3EC\uB9F7\uC785\uB2C8\uB2E4. \uD55C\uAE00 \uD504\uB85C\uADF8\uB7A8\uC5D0\uC11C .hwpx\uB85C \uC800\uC7A5 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.";
12226
+ }
12227
+ let zip;
12228
+ try {
12229
+ zip = await import_jszip.default.loadAsync(bytes);
12230
+ } catch (err) {
12231
+ return `\uC624\uB958: .hwpx ZIP\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${String(err)}`;
12232
+ }
12233
+ const sectionNames = Object.keys(zip.files).filter((name) => /^Contents\/section\d+\.xml$/.test(name)).sort();
12234
+ const xmls = [];
12235
+ for (const name of sectionNames) {
12236
+ try {
12237
+ const entry = zip.file(name);
12238
+ const xml = entry ? await entry.async("string") : "";
12239
+ xmls.push(xml);
12240
+ } catch {
12241
+ xmls.push("");
12146
12242
  }
12147
- } else if (raw.startsWith("<hp:cellSpan")) {
12148
- const colM = raw.match(/colSpan="(\d+)"/);
12149
- const rowM = raw.match(/rowSpan="(\d+)"/);
12150
- const selfClose = xml.indexOf("/>", pos);
12151
- const end = selfClose >= 0 ? selfClose + 2 : pos + raw.length;
12152
- tokens.push({
12153
- kind: "cell_span",
12154
- pos,
12155
- end,
12156
- colSpan: colM ? Number(colM[1]) : 1,
12157
- rowSpan: rowM ? Number(rowM[1]) : 1
12158
- });
12159
12243
  }
12160
- startPos = re.lastIndex;
12161
- m = re.exec(xml);
12244
+ if (xmls.length === 0) {
12245
+ return `\uC624\uB958: .hwpx \uD30C\uC77C\uC5D0\uC11C \uC139\uC158 XML\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}`;
12246
+ }
12247
+ const caseSensitive = input.caseSensitive ?? false;
12248
+ let hits;
12249
+ try {
12250
+ hits = findInSectionXmls(xmls, input.query, caseSensitive);
12251
+ } catch (err) {
12252
+ return `\uC624\uB958: \uBB38\uC11C \uD0D0\uC0C9 \uC911 \uC608\uC678\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4: ${String(err)}`;
12253
+ }
12254
+ if (hits.length === 0) {
12255
+ return `\uBB38\uC11C\uC5D0\uC11C "${input.query}"\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4: ${input.path}`;
12256
+ }
12257
+ return formatHits(hits, input.query);
12162
12258
  }
12163
- void startPos;
12164
- return tokens;
12165
- }
12259
+ };
12166
12260
  function escapeXml2(text3) {
12167
- return text3.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
12261
+ return text3.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
12168
12262
  }
12169
- function collectDirectTcRanges(tokens, tblStart, tblEnd) {
12170
- const tblTokens = tokens.filter((t) => t.pos > tblStart && t.pos < tblEnd);
12171
- const tcRanges = [];
12172
- const tcStack = [];
12173
- let innerDepth = 0;
12174
- for (const tok of tblTokens) {
12175
- if (tok.kind === "tbl_open") {
12176
- innerDepth++;
12177
- } else if (tok.kind === "tbl_close") {
12178
- innerDepth--;
12179
- } else if (tok.kind === "tc_open") {
12180
- tcStack.push({ pos: tok.pos, depth: innerDepth });
12181
- } else if (tok.kind === "tc_close") {
12182
- const entry = tcStack.pop();
12183
- if (entry !== void 0) {
12184
- tcRanges.push({ start: entry.pos, end: tok.end, depth: entry.depth });
12185
- }
12186
- }
12187
- }
12188
- return tcRanges.filter((tc) => tc.depth === 0);
12263
+ function decodeXml(text3) {
12264
+ return text3.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'");
12189
12265
  }
12190
- function readOwnTextFromTc(xml, tokens, tcStart, tcEnd) {
12191
- const tcTokens = tokens.filter((t) => t.pos >= tcStart && t.pos < tcEnd);
12192
- let innerDepth = 0;
12193
- let text3 = "";
12194
- for (const t of tcTokens) {
12195
- if (t.kind === "tbl_open") innerDepth++;
12196
- else if (t.kind === "tbl_close") innerDepth--;
12197
- else if (t.kind === "t_empty" && innerDepth === 0) {
12198
- } else if (t.kind === "t_open" && innerDepth === 0) {
12199
- const closePos = xml.indexOf("</hp:t>", t.end);
12200
- if (closePos >= 0) {
12201
- text3 += xml.substring(t.end, closePos);
12202
- }
12266
+ function getAttr(openTag, attr) {
12267
+ const re = new RegExp(`\\b${attr}="([^"]*)"`, "");
12268
+ const m = re.exec(openTag);
12269
+ return m ? decodeXml(m[1] ?? "") : "";
12270
+ }
12271
+ function parseFormObjects(xml, sectionFile, startIndex = 0) {
12272
+ const results = [];
12273
+ let idx = startIndex;
12274
+ const tagSpecs = [
12275
+ { xmlTag: "hp:btn", type: "button" },
12276
+ { xmlTag: "hp:checkBtn", type: "checkBox" },
12277
+ { xmlTag: "hp:radioBtn", type: "radioButton" },
12278
+ { xmlTag: "hp:comboBox", type: "comboBox" },
12279
+ { xmlTag: "hp:edit", type: "edit" }
12280
+ ];
12281
+ const hits = [];
12282
+ for (const spec of tagSpecs) {
12283
+ const re = new RegExp(`<${spec.xmlTag}\\b`, "g");
12284
+ for (let m = re.exec(xml); m !== null; m = re.exec(xml)) {
12285
+ hits.push({ pos: m.index, xmlTag: spec.xmlTag, type: spec.type });
12203
12286
  }
12204
12287
  }
12205
- return text3.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
12206
- }
12207
- function findTopLevelTableRange(tokens, tableIndex) {
12208
- let topLevelCount = 0;
12209
- let depth = 0;
12210
- let targetStart = -1;
12211
- for (const tok of tokens) {
12212
- if (tok.kind === "tbl_open") {
12213
- if (depth === 0) {
12214
- if (topLevelCount === tableIndex) {
12215
- targetStart = tok.pos;
12288
+ hits.sort((a, b) => a.pos - b.pos);
12289
+ for (const hit of hits) {
12290
+ const { pos, xmlTag, type } = hit;
12291
+ const openTagEnd = xml.indexOf(">", pos);
12292
+ if (openTagEnd < 0) continue;
12293
+ const openTag = xml.slice(pos, openTagEnd + 1);
12294
+ const closeTag = `</${xmlTag}>`;
12295
+ const closeIdx = xml.indexOf(closeTag, openTagEnd);
12296
+ if (closeIdx < 0) continue;
12297
+ const elementEnd = closeIdx + closeTag.length;
12298
+ const elementXml = xml.slice(pos, elementEnd);
12299
+ const name = getAttr(openTag, "name");
12300
+ let currentValue;
12301
+ let comboItems;
12302
+ switch (type) {
12303
+ case "button":
12304
+ currentValue = getAttr(openTag, "caption");
12305
+ break;
12306
+ case "checkBox":
12307
+ case "radioButton":
12308
+ currentValue = getAttr(openTag, "value") === "CHECKED";
12309
+ break;
12310
+ case "comboBox": {
12311
+ currentValue = getAttr(openTag, "selectedValue");
12312
+ const listItemRe = /<hp:listItem\b[^>]*/g;
12313
+ const items = [];
12314
+ for (let lm = listItemRe.exec(elementXml); lm !== null; lm = listItemRe.exec(elementXml)) {
12315
+ const itemValue = getAttr(lm[0] ?? "", "value");
12316
+ items.push(itemValue);
12216
12317
  }
12217
- topLevelCount++;
12218
- }
12219
- depth++;
12220
- } else if (tok.kind === "tbl_close") {
12221
- depth--;
12222
- if (depth === 0 && topLevelCount === tableIndex + 1 && targetStart >= 0) {
12223
- return { start: targetStart, end: tok.end };
12318
+ comboItems = items;
12319
+ break;
12320
+ }
12321
+ case "edit": {
12322
+ const textSelfClose = /<hp:text\s*\/>/;
12323
+ const textOpen = /<hp:text>/;
12324
+ const textClose = "</hp:text>";
12325
+ if (textSelfClose.test(elementXml)) {
12326
+ currentValue = "";
12327
+ } else {
12328
+ const tOpenIdx = elementXml.search(textOpen);
12329
+ if (tOpenIdx >= 0) {
12330
+ const afterOpen = elementXml.indexOf(">", tOpenIdx) + 1;
12331
+ const closePos = elementXml.indexOf(textClose, afterOpen);
12332
+ currentValue = closePos >= 0 ? decodeXml(elementXml.slice(afterOpen, closePos)) : "";
12333
+ } else {
12334
+ currentValue = "";
12335
+ }
12336
+ }
12337
+ break;
12224
12338
  }
12225
12339
  }
12340
+ results.push({
12341
+ index: idx++,
12342
+ name,
12343
+ type,
12344
+ currentValue,
12345
+ comboItems,
12346
+ sectionFile,
12347
+ posInSection: pos
12348
+ });
12226
12349
  }
12227
- return null;
12228
- }
12229
- function readCellAddrSpan(tokens, tcStart, tcEnd) {
12230
- const tcTokens = tokens.filter((t) => t.pos >= tcStart && t.pos < tcEnd);
12231
- let addr = null;
12232
- let span = { colSpan: 1, rowSpan: 1 };
12233
- for (const t of tcTokens) {
12234
- if (t.kind === "cell_addr" && t.colAddr !== void 0 && t.rowAddr !== void 0) {
12235
- addr = { colAddr: t.colAddr, rowAddr: t.rowAddr };
12236
- } else if (t.kind === "cell_span" && t.colSpan !== void 0 && t.rowSpan !== void 0) {
12237
- span = { colSpan: t.colSpan, rowSpan: t.rowSpan };
12238
- }
12239
- }
12240
- if (!addr) return null;
12241
- return { ...addr, ...span };
12350
+ return results;
12242
12351
  }
12243
- function applyCellEditsToSectionXml(xml, edits) {
12244
- const tokens = tokenizeHwpxXml(xml);
12352
+ function applyFormObjectEdits(xml, edits) {
12245
12353
  const results = edits.map(() => ({ success: false }));
12246
- let totalTopLevelTbls = 0;
12247
- {
12248
- let d = 0;
12249
- for (const tok of tokens) {
12250
- if (tok.kind === "tbl_open") {
12251
- if (d === 0) totalTopLevelTbls++;
12252
- d++;
12253
- } else if (tok.kind === "tbl_close") {
12254
- d--;
12255
- }
12256
- }
12257
- }
12258
- const replacements = [];
12354
+ const patches = [];
12259
12355
  for (let ei = 0; ei < edits.length; ei++) {
12260
12356
  const edit = edits[ei];
12261
- const tblRange = findTopLevelTableRange(tokens, edit.tableIndex);
12262
- if (!tblRange) {
12357
+ const { target, set, expected } = edit;
12358
+ const pos = target.posInSection;
12359
+ const xmlTag = typeToXmlTag(target.type);
12360
+ const openTagEnd = xml.indexOf(">", pos);
12361
+ if (openTagEnd < 0) {
12263
12362
  results[ei] = {
12264
12363
  success: false,
12265
- error: `\uD45C ${edit.tableIndex}\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774 \uC139\uC158\uC5D0 \uCD1D ${totalTopLevelTbls}\uAC1C\uC758 \uCD5C\uC0C1\uC704 \uD45C\uAC00 \uC788\uC2B5\uB2C8\uB2E4.`
12364
+ error: `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}" \uC5EC\uB294 \uD0DC\uADF8 \uB05D\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
12266
12365
  };
12267
12366
  continue;
12268
12367
  }
12269
- const directTcs = collectDirectTcRanges(tokens, tblRange.start, tblRange.end);
12270
- const tblTokens = tokens.filter((t) => t.pos >= tblRange.start && t.pos < tblRange.end);
12271
- let found = false;
12272
- for (const tc of directTcs) {
12273
- const tcTokens = tblTokens.filter((t) => t.pos >= tc.start && t.pos < tc.end);
12274
- const hasAddr = tcTokens.some(
12275
- (t) => t.kind === "cell_addr" && t.colAddr === edit.col && t.rowAddr === edit.row
12276
- );
12277
- if (!hasAddr) continue;
12278
- let innerDepth = 0;
12279
- const ownTRuns = [];
12280
- for (const t of tokens.filter((x) => x.pos >= tc.start && x.pos < tc.end)) {
12281
- if (t.kind === "tbl_open") innerDepth++;
12282
- else if (t.kind === "tbl_close") innerDepth--;
12283
- else if (t.kind === "t_empty" && innerDepth === 0) {
12284
- ownTRuns.push({ isEmpty: true, tagPos: t.pos, tagEnd: t.end });
12285
- } else if (t.kind === "t_open" && innerDepth === 0) {
12286
- const closePos = xml.indexOf("</hp:t>", t.end);
12287
- if (closePos >= 0) {
12288
- ownTRuns.push({ isEmpty: false, openEnd: t.end, closePos });
12289
- }
12290
- }
12368
+ const _openTag = xml.slice(pos, openTagEnd + 1);
12369
+ const closeTag = `</${xmlTag}>`;
12370
+ const closeIdx = xml.indexOf(closeTag, openTagEnd);
12371
+ if (closeIdx < 0) {
12372
+ results[ei] = {
12373
+ success: false,
12374
+ error: `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}" \uB2EB\uB294 \uD0DC\uADF8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
12375
+ };
12376
+ continue;
12377
+ }
12378
+ const elementEnd = closeIdx + closeTag.length;
12379
+ const typeError = validateSetForType(target.type, set);
12380
+ if (typeError) {
12381
+ results[ei] = { success: false, error: typeError };
12382
+ continue;
12383
+ }
12384
+ const currentRaw = readCurrentValue(
12385
+ xml,
12386
+ pos,
12387
+ openTagEnd,
12388
+ openTagEnd + 1,
12389
+ elementEnd,
12390
+ target.type
12391
+ );
12392
+ if (expected !== void 0) {
12393
+ const mismatch = checkExpected(expected, currentRaw, target);
12394
+ if (mismatch) {
12395
+ results[ei] = { success: false, error: mismatch };
12396
+ continue;
12291
12397
  }
12292
- const currentText = ownTRuns.map((r) => r.isEmpty ? "" : xml.substring(r.openEnd, r.closePos)).join("").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
12293
- if (edit.expectedText !== void 0 && edit.expectedText !== currentText) {
12398
+ }
12399
+ if (target.type === "comboBox" && set.selected !== void 0) {
12400
+ const items = target.comboItems ?? [];
12401
+ if (!items.includes(set.selected)) {
12402
+ const validList = items.map((v) => `"${v}"`).join(", ");
12294
12403
  results[ei] = {
12295
12404
  success: false,
12296
- oldText: currentText,
12297
- error: `\uC140 (\uD45C ${edit.tableIndex}, \uD589 ${edit.row}, \uC5F4 ${edit.col})\uC758 \uD604\uC7AC \uD14D\uC2A4\uD2B8\uAC00 \uC608\uC0C1\uAC12\uACFC \uB2E4\uB985\uB2C8\uB2E4. \uC608\uC0C1: "${edit.expectedText}", \uC2E4\uC81C: "${currentText}". \uC218\uC815\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`
12405
+ error: `comboBox "${target.name}"\uC758 selected \uAC12 "${set.selected}"\uC774 \uC720\uD6A8\uD55C \uD56D\uBAA9\uC774 \uC544\uB2D9\uB2C8\uB2E4. \uC720\uD6A8\uD55C \uD56D\uBAA9: ${validList || "(\uC5C6\uC74C)"}`
12298
12406
  };
12299
- found = true;
12300
- break;
12407
+ continue;
12301
12408
  }
12302
- if (ownTRuns.length === 0) {
12409
+ }
12410
+ let patchCreated = false;
12411
+ if (set.caption !== void 0) {
12412
+ const patch = replaceAttrInOpenTag(xml, pos, openTagEnd, "caption", escapeXml2(set.caption));
12413
+ if (!patch) {
12303
12414
  results[ei] = {
12304
12415
  success: false,
12305
- oldText: currentText,
12306
- error: `\uC140 (\uD45C ${edit.tableIndex}, \uD589 ${edit.row}, \uC5F4 ${edit.col})\uC5D0 \uD14D\uC2A4\uD2B8 \uB7F0\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. <hp:t> \uB610\uB294 <hp:t/> \uB7F0\uC774 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uC140 \uAD6C\uC870\uB97C \uD655\uC778\uD558\uC138\uC694.`
12416
+ error: `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}"\uC758 caption \uC18D\uC131\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
12307
12417
  };
12308
- found = true;
12309
- break;
12418
+ continue;
12310
12419
  }
12311
- const escapedNew = escapeXml2(edit.newText);
12312
- const patches = [];
12313
- const firstRun = ownTRuns[0];
12314
- if (firstRun.isEmpty) {
12315
- patches.push({
12316
- from: firstRun.tagPos,
12317
- to: firstRun.tagEnd,
12318
- text: `<hp:t>${escapedNew}</hp:t>`
12319
- });
12320
- } else {
12321
- patches.push({ from: firstRun.openEnd, to: firstRun.closePos, text: escapedNew });
12420
+ patches.push(patch);
12421
+ patchCreated = true;
12422
+ } else if (set.checked !== void 0) {
12423
+ const newValue = set.checked ? "CHECKED" : "UNCHECKED";
12424
+ const patch = replaceAttrInOpenTag(xml, pos, openTagEnd, "value", newValue);
12425
+ if (!patch) {
12426
+ results[ei] = {
12427
+ success: false,
12428
+ error: `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}"\uC758 value \uC18D\uC131\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
12429
+ };
12430
+ continue;
12322
12431
  }
12323
- for (let ri = 1; ri < ownTRuns.length; ri++) {
12324
- const run = ownTRuns[ri];
12325
- if (!run.isEmpty) {
12326
- patches.push({ from: run.openEnd, to: run.closePos, text: "" });
12327
- }
12432
+ patches.push(patch);
12433
+ patchCreated = true;
12434
+ } else if (set.selected !== void 0) {
12435
+ const patch = replaceAttrInOpenTag(
12436
+ xml,
12437
+ pos,
12438
+ openTagEnd,
12439
+ "selectedValue",
12440
+ escapeXml2(set.selected)
12441
+ );
12442
+ if (!patch) {
12443
+ results[ei] = {
12444
+ success: false,
12445
+ error: `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}"\uC758 selectedValue \uC18D\uC131\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
12446
+ };
12447
+ continue;
12328
12448
  }
12329
- replacements.push({ editIdx: ei, patches });
12330
- results[ei] = { success: true, oldText: currentText };
12331
- found = true;
12332
- break;
12449
+ patches.push(patch);
12450
+ patchCreated = true;
12451
+ } else if (set.text !== void 0) {
12452
+ const elementContent = xml.slice(pos, elementEnd);
12453
+ const textPatch = replaceEditText(xml, pos, elementContent, set.text);
12454
+ if (!textPatch) {
12455
+ results[ei] = {
12456
+ success: false,
12457
+ error: `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}"\uC758 <hp:text> \uC694\uC18C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
12458
+ };
12459
+ continue;
12460
+ }
12461
+ patches.push(textPatch);
12462
+ patchCreated = true;
12333
12463
  }
12334
- if (!found) {
12335
- results[ei] = {
12336
- success: false,
12337
- error: `\uD45C ${edit.tableIndex}\uC5D0\uC11C \uC140 (\uD589 ${edit.row}, \uC5F4 ${edit.col})\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. cellAddr colAddr="${edit.col}" rowAddr="${edit.row}"\uC5D0 \uD574\uB2F9\uD558\uB294 \uC140\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.`
12338
- };
12464
+ if (!patchCreated) {
12465
+ results[ei] = { success: false, error: `\uD3B8\uC9D1 #${ei + 1}: set \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.` };
12466
+ continue;
12467
+ }
12468
+ results[ei] = { success: true, oldValue: currentRaw };
12469
+ }
12470
+ if (results.some((r) => !r.success)) {
12471
+ return { newXml: xml, results };
12472
+ }
12473
+ const sortedPatches = [...patches].sort((a, b) => b.from - a.from);
12474
+ let result = xml;
12475
+ for (const patch of sortedPatches) {
12476
+ result = result.slice(0, patch.from) + patch.text + result.slice(patch.to);
12477
+ }
12478
+ return { newXml: result, results };
12479
+ }
12480
+ function typeToXmlTag(type) {
12481
+ switch (type) {
12482
+ case "button":
12483
+ return "hp:btn";
12484
+ case "checkBox":
12485
+ return "hp:checkBtn";
12486
+ case "radioButton":
12487
+ return "hp:radioBtn";
12488
+ case "comboBox":
12489
+ return "hp:comboBox";
12490
+ case "edit":
12491
+ return "hp:edit";
12492
+ }
12493
+ }
12494
+ function validateSetForType(type, set) {
12495
+ const key = Object.keys(set).find((k) => set[k] !== void 0);
12496
+ if (!key) return "set \uD544\uB4DC\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4. caption/checked/selected/text \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD558\uC138\uC694.";
12497
+ const allowed = {
12498
+ button: "caption",
12499
+ checkBox: "checked",
12500
+ radioButton: "checked",
12501
+ comboBox: "selected",
12502
+ edit: "text"
12503
+ };
12504
+ if (key !== allowed[type]) {
12505
+ const typeKo = {
12506
+ button: "PushButton",
12507
+ checkBox: "CheckBox",
12508
+ radioButton: "RadioButton",
12509
+ comboBox: "ComboBox",
12510
+ edit: "Edit"
12511
+ };
12512
+ return `\uD0C0\uC785 \uBD88\uC77C\uCE58: ${typeKo[type]} \uC591\uC2DD \uAC1C\uCCB4\uC5D0\uB294 "${allowed[type]}" \uD544\uB4DC\uB9CC \uC0AC\uC6A9 \uAC00\uB2A5\uD558\uC9C0\uB9CC "${key}"\uC774(\uAC00) \uC9C0\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`;
12513
+ }
12514
+ return null;
12515
+ }
12516
+ function readCurrentValue(xml, pos, openTagEnd, _afterOpen, elementEnd, type) {
12517
+ const openTag = xml.slice(pos, openTagEnd + 1);
12518
+ switch (type) {
12519
+ case "button":
12520
+ return decodeXml(getAttr(openTag, "caption"));
12521
+ case "checkBox":
12522
+ case "radioButton":
12523
+ return getAttr(openTag, "value") === "CHECKED";
12524
+ case "comboBox":
12525
+ return decodeXml(getAttr(openTag, "selectedValue"));
12526
+ case "edit": {
12527
+ const elementContent = xml.slice(pos, elementEnd);
12528
+ if (/<hp:text\s*\/>/.test(elementContent)) return "";
12529
+ const tOpenIdx = elementContent.search(/<hp:text>/);
12530
+ if (tOpenIdx >= 0) {
12531
+ const afterOpen = elementContent.indexOf(">", tOpenIdx) + 1;
12532
+ const closePos = elementContent.indexOf("</hp:text>", afterOpen);
12533
+ return closePos >= 0 ? decodeXml(elementContent.slice(afterOpen, closePos)) : "";
12534
+ }
12535
+ return "";
12339
12536
  }
12340
12537
  }
12341
- if (results.some((r) => !r.success)) {
12342
- return { newXml: xml, results };
12538
+ }
12539
+ function checkExpected(expected, currentRaw, target) {
12540
+ let expectedVal;
12541
+ if (expected.caption !== void 0) expectedVal = expected.caption;
12542
+ else if (expected.checked !== void 0) expectedVal = expected.checked;
12543
+ else if (expected.selected !== void 0) expectedVal = expected.selected;
12544
+ else if (expected.text !== void 0) expectedVal = expected.text;
12545
+ if (expectedVal === void 0) return null;
12546
+ const matches = typeof expectedVal === "boolean" ? currentRaw === expectedVal : String(currentRaw) === String(expectedVal);
12547
+ if (!matches) {
12548
+ return `\uC591\uC2DD \uAC1C\uCCB4 "${target.name}" \uD604\uC7AC \uAC12\uC774 \uC608\uC0C1\uAC12\uACFC \uB2E4\uB985\uB2C8\uB2E4. \uC608\uC0C1: ${JSON.stringify(expectedVal)}, \uC2E4\uC81C: ${JSON.stringify(currentRaw)}. \uC218\uC815\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`;
12343
12549
  }
12344
- const allPatches = replacements.flatMap((r) => r.patches).sort((a, b) => b.from - a.from);
12345
- let result = xml;
12346
- for (const patch of allPatches) {
12347
- result = result.substring(0, patch.from) + patch.text + result.substring(patch.to);
12550
+ return null;
12551
+ }
12552
+ function replaceAttrInOpenTag(xml, tagStart, tagEnd, attr, newValue) {
12553
+ const tagStr = xml.slice(tagStart, tagEnd + 1);
12554
+ const re = new RegExp(`\\b(${attr}=")([^"]*)(")`, "");
12555
+ const m = re.exec(tagStr);
12556
+ if (!m) return null;
12557
+ const relFrom = m.index + (m[1]?.length ?? 0);
12558
+ const relTo = relFrom + (m[2]?.length ?? 0);
12559
+ return {
12560
+ from: tagStart + relFrom,
12561
+ to: tagStart + relTo,
12562
+ text: newValue
12563
+ };
12564
+ }
12565
+ function replaceEditText(_xml, pos, elementContent, newText) {
12566
+ const escaped = escapeXml2(newText);
12567
+ const selfCloseRe = /<hp:text\s*\/>/;
12568
+ const scm = selfCloseRe.exec(elementContent);
12569
+ if (scm) {
12570
+ return {
12571
+ from: pos + scm.index,
12572
+ to: pos + scm.index + scm[0].length,
12573
+ text: `<hp:text>${escaped}</hp:text>`
12574
+ };
12348
12575
  }
12349
- return { newXml: result, results };
12576
+ const openRe = /<hp:text>/;
12577
+ const om = openRe.exec(elementContent);
12578
+ if (om) {
12579
+ const afterOpen = om.index + om[0].length;
12580
+ const closePos = elementContent.indexOf("</hp:text>", afterOpen);
12581
+ if (closePos < 0) return null;
12582
+ return {
12583
+ from: pos + afterOpen,
12584
+ to: pos + closePos,
12585
+ text: escaped
12586
+ };
12587
+ }
12588
+ return null;
12350
12589
  }
12351
- async function applyEditsToHwpx(hwpxBuffer, edits) {
12352
- const zip = await import_jszip2.default.loadAsync(hwpxBuffer);
12590
+ async function listFormObjectsFromZip(zip) {
12353
12591
  const sectionFiles = Object.keys(zip.files).filter((name) => /^Contents\/section\d+\.xml$/.test(name)).sort();
12354
12592
  const sectionXmls = [];
12355
- const sectionTblCounts = [];
12593
+ const objects = [];
12594
+ let globalIndex = 0;
12356
12595
  for (const sf of sectionFiles) {
12357
12596
  const entry = zip.file(sf);
12358
12597
  const xml = entry ? await entry.async("string") : "";
12359
12598
  sectionXmls.push(xml);
12360
- const tokens = tokenizeHwpxXml(xml);
12361
- let count = 0;
12362
- let depth = 0;
12363
- for (const tok of tokens) {
12364
- if (tok.kind === "tbl_open") {
12365
- if (depth === 0) count++;
12366
- depth++;
12367
- } else if (tok.kind === "tbl_close") {
12368
- depth--;
12369
- }
12370
- }
12371
- sectionTblCounts.push(count);
12372
- }
12373
- const sectionEdits = sectionFiles.map(() => []);
12374
- let offset = 0;
12375
- for (let si = 0; si < sectionFiles.length; si++) {
12376
- const count = sectionTblCounts[si] ?? 0;
12377
- for (let ei = 0; ei < edits.length; ei++) {
12378
- const edit = edits[ei];
12379
- if (edit.tableIndex >= offset && edit.tableIndex < offset + count) {
12380
- const secEdits = sectionEdits[si];
12381
- if (secEdits) {
12382
- secEdits.push({
12383
- tableIndex: edit.tableIndex - offset,
12384
- // 섹션 내 상대 인덱스
12385
- row: edit.row,
12386
- col: edit.col,
12387
- newText: edit.newText,
12388
- expectedText: edit.expectedText,
12389
- originalEditIdx: ei
12390
- });
12391
- }
12392
- }
12393
- }
12394
- offset += count;
12395
- }
12396
- const totalTables = sectionTblCounts.reduce((a, b) => a + b, 0);
12397
- const allResults = edits.map((edit, ei) => ({
12398
- success: false,
12399
- error: `\uD45C ${edit?.tableIndex ?? ei}\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uC11C\uC5D0 \uCD1D ${totalTables}\uAC1C\uC758 \uCD5C\uC0C1\uC704 \uD45C\uAC00 \uC788\uC2B5\uB2C8\uB2E4.`
12400
- }));
12401
- const newSectionXmls = [...sectionXmls];
12402
- for (let si = 0; si < sectionFiles.length; si++) {
12403
- const sEdits = sectionEdits[si] ?? [];
12404
- if (sEdits.length === 0) continue;
12405
- const srcXml = sectionXmls[si] ?? "";
12406
- const { newXml, results } = applyCellEditsToSectionXml(srcXml, sEdits);
12407
- newSectionXmls[si] = newXml;
12408
- for (let i = 0; i < sEdits.length; i++) {
12409
- const sEdit = sEdits[i];
12410
- const res = results[i];
12411
- if (sEdit && res) {
12412
- allResults[sEdit.originalEditIdx] = res;
12413
- }
12414
- }
12599
+ const parsed = parseFormObjects(xml, sf, globalIndex);
12600
+ objects.push(...parsed);
12601
+ globalIndex += parsed.length;
12415
12602
  }
12416
- if (allResults.some((r) => !r.success)) {
12417
- return { buffer: hwpxBuffer, results: allResults };
12603
+ return { objects, sectionFiles, sectionXmls };
12604
+ }
12605
+ function validateHwpxBuffer(ext, buffer) {
12606
+ if (ext === ".hwp") {
12607
+ return "\uC624\uB958: \uC774 \uD234\uC740 .hwpx \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. .hwp(\uAD6C\uD615 OLE \uBC14\uC774\uB108\uB9AC)\uB294 \uC9C1\uC811 \uD3B8\uC9D1\uC774 \uBD88\uAC00\uD569\uB2C8\uB2E4. \uD55C\uAE00 \uD504\uB85C\uADF8\uB7A8\uC5D0\uC11C '\uB2E4\uB978 \uC774\uB984\uC73C\uB85C \uC800\uC7A5 \u2192 .hwpx'\uB85C \uC800\uC7A5\uD55C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.";
12418
12608
  }
12419
- const out = new import_jszip2.default();
12420
- const mimetypeEntry = zip.file("mimetype");
12421
- if (mimetypeEntry) {
12422
- out.file("mimetype", await mimetypeEntry.async("uint8array"), { compression: "STORE" });
12609
+ if (ext !== ".hwpx") {
12610
+ return `\uC624\uB958: \uC774 \uD234\uC740 .hwpx \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. \uD604\uC7AC \uD30C\uC77C \uD655\uC7A5\uC790: ${ext}`;
12423
12611
  }
12424
- for (const [name, entry] of Object.entries(zip.files)) {
12425
- if (name === "mimetype" || entry.dir) continue;
12426
- const sectionIdx = sectionFiles.indexOf(name);
12427
- if (sectionIdx >= 0) {
12428
- out.file(name, newSectionXmls[sectionIdx] ?? "");
12429
- } else {
12430
- out.file(name, await entry.async("uint8array"));
12431
- }
12612
+ if (buffer[0] !== 80 || buffer[1] !== 75) {
12613
+ return "\uC624\uB958: \uD30C\uC77C\uC774 \uC720\uD6A8\uD55C .hwpx(ZIP) \uD3EC\uB9F7\uC774 \uC544\uB2D9\uB2C8\uB2E4. \uD30C\uC77C\uC774 \uC190\uC0C1\uB418\uC5C8\uAC70\uB098 \uAD6C\uD615 .hwp(OLE \uBC14\uC774\uB108\uB9AC) \uD3EC\uB9F7\uC785\uB2C8\uB2E4.";
12432
12614
  }
12433
- const buf = await out.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
12434
- return { buffer: new Uint8Array(buf), results: allResults };
12615
+ return null;
12435
12616
  }
12436
- var proposeCellEditTool = {
12437
- name: "propose_cell_edit",
12438
- description: "HWPX \uBB38\uC11C\uC758 \uD45C \uC140 \uB0B4\uC6A9\uC744 XML \uC9C1\uC811 \uD328\uCE58 \uBC29\uC2DD\uC73C\uB85C \uC218\uC815\uD569\uB2C8\uB2E4. \uBCD1\uD569 \uC140(cellSpan/rowSpan)\uC774 \uC788\uB294 \uD45C\uC5D0\uC11C\uB3C4 \uBCD1\uD569 \uAD6C\uC870\uB97C \uC644\uC804\uD788 \uBCF4\uC874\uD569\uB2C8\uB2E4. \uBE48 \uC140(<hp:t/> self-closing \uB7F0)\uB3C4 \uCC44\uC6B8 \uC218 \uC788\uC5B4 \uC591\uC2DD(form) \uD3B8\uC9D1\uC5D0 \uC801\uD569\uD569\uB2C8\uB2E4. \uC140 \uC8FC\uC18C \uC9C0\uC815 \uBC29\uBC95: (1) \uC88C\uD45C \uBAA8\uB4DC \u2014 tableIndex + row + col \uC9C1\uC811 \uC9C0\uC815. (2) \uB808\uC774\uBE14 \uBAA8\uB4DC \u2014 label(\uC778\uC811 \uB808\uC774\uBE14 \uC140 \uD14D\uC2A4\uD2B8) + direction(right/below, \uAE30\uBCF8 right) + \uC120\uD0DD\uC801 tableIndex\uB85C \uB808\uC774\uBE14 \uC606/\uC544\uB798 \uC140\uC744 \uC790\uB3D9\uC73C\uB85C \uCC3E\uC544 \uD3B8\uC9D1. \uBCD1\uD569 \uB808\uC774\uBE14 \uC140\uB3C4 colSpan/rowSpan\uC744 \uBC18\uC601\uD558\uC5EC \uB300\uC0C1 \uC140\uC744 \uACC4\uC0B0\uD569\uB2C8\uB2E4. propose_edit/propose_form_fill\uC740 \uB9C8\uD06C\uB2E4\uC6B4 \uB77C\uC6B4\uB4DC\uD2B8\uB9BD\uC73C\uB85C \uBCD1\uD569 \uC140\uC744 \uC18C\uC2E4\uC2DC\uD0A4\uBBC0\uB85C, \uBCD1\uD569 \uC140\uC774 \uC788\uB294 \uD45C\uB97C \uC218\uC815\uD560 \uB54C\uB294 \uC774 \uD234\uC744 \uC0AC\uC6A9\uD558\uC138\uC694. .hwpx \uD30C\uC77C \uC804\uC6A9\uC785\uB2C8\uB2E4. .hwp\uB294 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC73C\uBA70 Hancom\uC5D0\uC11C .hwpx\uB85C \uC800\uC7A5 \uD6C4 \uC0AC\uC6A9\uD558\uC138\uC694. \uC218\uC815 \uC804\uC5D0 \uBC18\uB4DC\uC2DC read_document\uB85C \uC6D0\uBCF8\uC744 \uC77D\uACE0, expectedText\uB97C \uC9C0\uC815\uD558\uC5EC \uC548\uC804\uD558\uAC8C \uC218\uC815\uD558\uC138\uC694. \uBCC0\uACBD \uC0AC\uD56D\uC740 diff \uBBF8\uB9AC\uBCF4\uAE30\uC640 \uD568\uAED8 \uC0AC\uC6A9\uC790 \uC2B9\uC778\uC744 \uBC1B\uC740 \uD6C4\uC5D0\uB9CC \uC800\uC7A5\uB429\uB2C8\uB2E4.",
12439
- inputSchema: proposeCellEditSchema,
12440
- requiresApproval: true,
12441
- propose: async ({
12442
- input,
12443
- ctx
12444
- }) => {
12445
- const safePath = await resolveSafePath(ctx.cwd, input.path);
12446
- const ext = extname5(safePath).toLowerCase();
12447
- if (ext === ".hwp") {
12448
- return "\uC624\uB958: propose_cell_edit\uC740 .hwpx \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. .hwp(\uAD6C\uD615 OLE \uBC14\uC774\uB108\uB9AC)\uB294 \uC9C1\uC811 \uD3B8\uC9D1\uC774 \uBD88\uAC00\uD569\uB2C8\uB2E4. \uD55C\uAE00 \uD504\uB85C\uADF8\uB7A8\uC5D0\uC11C '\uB2E4\uB978 \uC774\uB984\uC73C\uB85C \uC800\uC7A5 \u2192 .hwpx'\uB85C \uC800\uC7A5\uD55C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694. \uB610\uB294 propose_edit\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC788\uC73C\uB098, \uBCD1\uD569 \uC140\uC774 \uC18C\uC2E4\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.";
12449
- }
12450
- if (ext !== ".hwpx") {
12451
- return `\uC624\uB958: propose_cell_edit\uC740 .hwpx \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. \uD604\uC7AC \uD30C\uC77C: ${ext}. \uD45C \uC140 \uC9C1\uC811 \uD3B8\uC9D1\uC740 .hwpx \uD3EC\uB9F7\uC5D0\uC11C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4.`;
12452
- }
12453
- let originalBuffer;
12454
- try {
12455
- originalBuffer = await readFile5(safePath);
12456
- } catch {
12457
- return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}. \uACBD\uB85C\uB97C \uD655\uC778\uD558\uAC70\uB098 read_document\uB85C \uBA3C\uC800 \uD655\uC778\uD558\uC138\uC694.`;
12458
- }
12459
- if (originalBuffer[0] !== 80 || originalBuffer[1] !== 75) {
12460
- return "\uC624\uB958: \uD30C\uC77C\uC774 \uC720\uD6A8\uD55C .hwpx(ZIP) \uD3EC\uB9F7\uC774 \uC544\uB2D9\uB2C8\uB2E4. \uD30C\uC77C\uC774 \uC190\uC0C1\uB418\uC5C8\uAC70\uB098 \uAD6C\uD615 .hwp(OLE \uBC14\uC774\uB108\uB9AC) \uD3EC\uB9F7\uC785\uB2C8\uB2E4. \uD55C\uAE00 \uD504\uB85C\uADF8\uB7A8\uC5D0\uC11C .hwpx\uB85C \uC800\uC7A5 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.";
12461
- }
12462
- const zipForLabel = await import_jszip2.default.loadAsync(new Uint8Array(originalBuffer.buffer));
12463
- const sectionFilesForLabel = Object.keys(zipForLabel.files).filter((name) => /^Contents\/section\d+\.xml$/.test(name)).sort();
12464
- const sectionInfos = [];
12465
- let globalTblOffset = 0;
12466
- for (const sf of sectionFilesForLabel) {
12467
- const entry = zipForLabel.file(sf);
12468
- const xml = entry ? await entry.async("string") : "";
12469
- const tokens = tokenizeHwpxXml(xml);
12470
- let count = 0;
12471
- let d = 0;
12472
- for (const tok of tokens) {
12473
- if (tok.kind === "tbl_open") {
12474
- if (d === 0) count++;
12475
- d++;
12476
- } else if (tok.kind === "tbl_close") {
12477
- d--;
12478
- }
12479
- }
12480
- sectionInfos.push({ xml, tblCount: count, globalOffset: globalTblOffset });
12481
- globalTblOffset += count;
12482
- }
12483
- function resolveLabelAcrossSections(label, direction, scopedTableIndex) {
12484
- const trimmedLabel = label.trim();
12485
- const allMatches = [];
12486
- for (const si of sectionInfos) {
12487
- const tokens = tokenizeHwpxXml(si.xml);
12488
- let localStart = 0;
12489
- let localEnd = si.tblCount - 1;
12490
- if (scopedTableIndex !== void 0) {
12491
- const localIdx2 = scopedTableIndex - si.globalOffset;
12492
- if (localIdx2 < 0 || localIdx2 >= si.tblCount) continue;
12493
- localStart = localIdx2;
12494
- localEnd = localIdx2;
12495
- }
12496
- for (let li = localStart; li <= localEnd; li++) {
12497
- const tblRange = findTopLevelTableRange(tokens, li);
12498
- if (!tblRange) continue;
12499
- const directTcs = collectDirectTcRanges(tokens, tblRange.start, tblRange.end);
12500
- for (const tc of directTcs) {
12501
- const cellText = readOwnTextFromTc(si.xml, tokens, tc.start, tc.end).trim();
12502
- if (cellText === trimmedLabel) {
12503
- const addrSpan2 = readCellAddrSpan(tokens, tc.start, tc.end);
12504
- if (addrSpan2) {
12505
- allMatches.push({
12506
- globalTableIndex: si.globalOffset + li,
12507
- addrSpan: addrSpan2,
12508
- sectionXml: si.xml,
12509
- sectionOffset: si.globalOffset
12510
- });
12511
- }
12512
- }
12513
- }
12514
- }
12515
- }
12516
- if (allMatches.length === 0) {
12517
- const scope = scopedTableIndex !== void 0 ? `\uD45C ${scopedTableIndex}` : "\uBB38\uC11C \uB0B4 \uBAA8\uB4E0 \uD45C";
12518
- return {
12519
- error: `\uB808\uC774\uBE14 "${label}"\uC744(\uB97C) ${scope}\uC5D0\uC11C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. read_document\uB85C \uD45C \uB0B4\uC6A9\uC744 \uD655\uC778\uD558\uC138\uC694.`
12520
- };
12521
- }
12522
- if (allMatches.length > 1) {
12523
- const locs = allMatches.map(
12524
- (m) => `\uD45C ${m.globalTableIndex} (\uD589 ${m.addrSpan.rowAddr}, \uC5F4 ${m.addrSpan.colAddr})`
12525
- ).join(", ");
12526
- return {
12527
- error: `\uB808\uC774\uBE14 "${label}"\uC774(\uAC00) \uC5EC\uB7EC \uC140\uC5D0\uC11C \uBC1C\uACAC\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${locs}. tableIndex\uB85C \uD0D0\uC0C9 \uBC94\uC704\uB97C \uC881\uD788\uAC70\uB098 \uC88C\uD45C(row/col)\uB97C \uC9C1\uC811 \uC9C0\uC815\uD558\uC138\uC694.`
12528
- };
12529
- }
12530
- const match = allMatches[0];
12531
- const { addrSpan, globalTableIndex, sectionXml, sectionOffset } = match;
12532
- let targetRow;
12533
- let targetCol;
12534
- if (direction === "right") {
12535
- targetRow = addrSpan.rowAddr;
12536
- targetCol = addrSpan.colAddr + addrSpan.colSpan;
12617
+ var listFormObjectsSchema = z5.object({
12618
+ path: z5.string().describe("\uC77D\uC744 .hwpx \uD30C\uC77C \uACBD\uB85C")
12619
+ });
12620
+ var formEditSetSchema = z5.object({
12621
+ caption: z5.string().optional().describe("PushButton \uCEA1\uC158 \uD14D\uC2A4\uD2B8"),
12622
+ checked: z5.boolean().optional().describe("CheckBox/RadioButton \uCCB4\uD06C \uC0C1\uD0DC (true=CHECKED)"),
12623
+ selected: z5.string().optional().describe("ComboBox \uC120\uD0DD \uAC12 (listItem \uC911 \uD558\uB098\uC5EC\uC57C \uD568)"),
12624
+ text: z5.string().optional().describe("Edit \uD14D\uC2A4\uD2B8 \uB0B4\uC6A9")
12625
+ }).refine(
12626
+ (v) => {
12627
+ const keys = ["caption", "checked", "selected", "text"].filter(
12628
+ (k) => v[k] !== void 0
12629
+ );
12630
+ return keys.length === 1;
12631
+ },
12632
+ { message: "set \uD544\uB4DC\uB294 caption/checked/selected/text \uC911 \uC815\uD655\uD788 \uD558\uB098\uB9CC \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4." }
12633
+ );
12634
+ var formEditExpectedSchema = z5.object({
12635
+ caption: z5.string().optional(),
12636
+ checked: z5.boolean().optional(),
12637
+ selected: z5.string().optional(),
12638
+ text: z5.string().optional()
12639
+ }).optional();
12640
+ var formEditItemSchema = z5.object({
12641
+ name: z5.string().describe("\uC591\uC2DD \uAC1C\uCCB4\uC758 name \uC18D\uC131 \uAC12"),
12642
+ index: z5.number().int().nonnegative().optional().describe("\uB3D9\uC77C name\uC774 \uC5EC\uB7FF\uC778 \uACBD\uC6B0 \uBB38\uC11C \uC804\uCCB4 0-based \uC778\uB371\uC2A4\uB85C \uAD6C\uBD84"),
12643
+ set: formEditSetSchema.describe("\uBCC0\uACBD\uD560 \uAC12 (caption/checked/selected/text \uC911 \uD558\uB098)"),
12644
+ expected: formEditExpectedSchema.describe(
12645
+ "\uD604\uC7AC \uAC12 \uC0AC\uC804 \uAC80\uC99D (\uC548\uC804 \uC635\uC158). \uC2E4\uC81C \uAC12\uC774 \uB2E4\uB974\uBA74 \uC774 \uD3B8\uC9D1\uC744 \uCDE8\uC18C\uD558\uACE0 \uC624\uB958 \uBC18\uD658."
12646
+ )
12647
+ });
12648
+ var proposeFormObjectSchema = z5.object({
12649
+ path: z5.string().describe("\uC218\uC815\uD560 .hwpx \uD30C\uC77C \uACBD\uB85C"),
12650
+ edits: z5.array(formEditItemSchema).min(1).describe("\uC591\uC2DD \uAC1C\uCCB4 \uD3B8\uC9D1 \uBAA9\uB85D"),
12651
+ summary: z5.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
12652
+ });
12653
+ var listFormObjectsTool = {
12654
+ name: "list_form_objects",
12655
+ description: "HWPX \uBB38\uC11C\uC758 \uC591\uC2DD \uAC1C\uCCB4(form object) \uBAA9\uB85D\uC744 \uC5F4\uAC70\uD569\uB2C8\uB2E4. PushButton, CheckBox, RadioButton, ComboBox, Edit \uB2E4\uC12F \uAC00\uC9C0 \uD0C0\uC785\uC744 \uC9C0\uC6D0\uD569\uB2C8\uB2E4. \uAC01 \uC591\uC2DD \uAC1C\uCCB4\uC758 \uC774\uB984(name), \uD0C0\uC785, \uD604\uC7AC \uAC12, ComboBox\uC758 \uACBD\uC6B0 \uC120\uD0DD \uAC00\uB2A5\uD55C \uD56D\uBAA9 \uBAA9\uB85D\uC744 \uBC18\uD658\uD569\uB2C8\uB2E4. propose_form_object\uB85C \uAC12\uC744 \uC218\uC815\uD558\uAE30 \uC804\uC5D0 \uC774 \uD234\uB85C \uBA3C\uC800 \uD604\uC7AC \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. .hwpx \uD30C\uC77C \uC804\uC6A9\uC785\uB2C8\uB2E4.",
12656
+ inputSchema: listFormObjectsSchema,
12657
+ requiresApproval: false,
12658
+ execute: async ({
12659
+ input,
12660
+ ctx
12661
+ }) => {
12662
+ const safePath = await resolveSafePath(ctx.cwd, input.path);
12663
+ const ext = extname5(safePath).toLowerCase();
12664
+ let buffer;
12665
+ try {
12666
+ buffer = await readFile6(safePath);
12667
+ } catch {
12668
+ return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}`;
12669
+ }
12670
+ const validationError = validateHwpxBuffer(ext, new Uint8Array(buffer.buffer));
12671
+ if (validationError) return validationError;
12672
+ let zip;
12673
+ try {
12674
+ zip = await import_jszip3.default.loadAsync(new Uint8Array(buffer.buffer));
12675
+ } catch {
12676
+ return "\uC624\uB958: .hwpx(ZIP) \uD30C\uC77C\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD30C\uC77C\uC774 \uC190\uC0C1\uB418\uC5C8\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.";
12677
+ }
12678
+ const { objects } = await listFormObjectsFromZip(zip);
12679
+ if (objects.length === 0) {
12680
+ return "\uC774 \uBB38\uC11C\uC5D0\uB294 \uC591\uC2DD \uAC1C\uCCB4(form object)\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.";
12681
+ }
12682
+ const lines = [`\uCD1D ${objects.length}\uAC1C\uC758 \uC591\uC2DD \uAC1C\uCCB4\uAC00 \uBC1C\uACAC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
12683
+ `];
12684
+ const typeKo = {
12685
+ button: "PushButton",
12686
+ checkBox: "CheckBox",
12687
+ radioButton: "RadioButton",
12688
+ comboBox: "ComboBox",
12689
+ edit: "Edit"
12690
+ };
12691
+ for (const obj of objects) {
12692
+ let valueStr;
12693
+ if (typeof obj.currentValue === "boolean") {
12694
+ valueStr = obj.currentValue ? "\uCCB4\uD06C\uB428 (CHECKED)" : "\uCCB4\uD06C \uD574\uC81C\uB428 (UNCHECKED)";
12537
12695
  } else {
12538
- targetRow = addrSpan.rowAddr + addrSpan.rowSpan;
12539
- targetCol = addrSpan.colAddr;
12540
- }
12541
- const localIdx = globalTableIndex - sectionOffset;
12542
- const tokens2 = tokenizeHwpxXml(sectionXml);
12543
- const tblRange2 = findTopLevelTableRange(tokens2, localIdx);
12544
- if (!tblRange2) {
12545
- return { error: `\uD45C ${globalTableIndex}\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uB0B4\uBD80 \uC624\uB958).` };
12546
- }
12547
- const directTcs2 = collectDirectTcRanges(tokens2, tblRange2.start, tblRange2.end);
12548
- const tblTokens2 = tokens2.filter((t) => t.pos >= tblRange2.start && t.pos < tblRange2.end);
12549
- let targetExists = false;
12550
- for (const tc of directTcs2) {
12551
- const tcTokens = tblTokens2.filter((t) => t.pos >= tc.start && t.pos < tc.end);
12552
- if (tcTokens.some(
12553
- (t) => t.kind === "cell_addr" && t.colAddr === targetCol && t.rowAddr === targetRow
12554
- )) {
12555
- targetExists = true;
12556
- break;
12557
- }
12696
+ valueStr = `"${obj.currentValue}"`;
12558
12697
  }
12559
- if (!targetExists) {
12560
- const dirLabel = direction === "right" ? "\uC624\uB978\uCABD" : "\uC544\uB798";
12561
- return {
12562
- error: `\uB808\uC774\uBE14 "${label}" (\uD45C ${globalTableIndex}, \uD589 ${addrSpan.rowAddr}, \uC5F4 ${addrSpan.colAddr})\uC758 ${dirLabel} \uC140 (\uD589 ${targetRow}, \uC5F4 ${targetCol})\uC774 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. direction \uB610\uB294 \uC88C\uD45C\uB97C \uD655\uC778\uD558\uC138\uC694.`
12563
- };
12698
+ let line = `[${obj.index}] name="${obj.name}" | \uD0C0\uC785: ${typeKo[obj.type]} | \uD604\uC7AC \uAC12: ${valueStr}`;
12699
+ if (obj.type === "comboBox" && obj.comboItems) {
12700
+ const itemList = obj.comboItems.map((v) => `"${v}"`).join(", ");
12701
+ line += ` | \uD56D\uBAA9: [${itemList}]`;
12564
12702
  }
12565
- return { tableIndex: globalTableIndex, row: targetRow, col: targetCol };
12703
+ lines.push(line);
12704
+ }
12705
+ return lines.join("\n");
12706
+ }
12707
+ };
12708
+ var proposeFormObjectTool = {
12709
+ name: "propose_form_object",
12710
+ description: "HWPX \uBB38\uC11C\uC758 \uC591\uC2DD \uAC1C\uCCB4(form object) \uAC12\uC744 XML \uC9C1\uC811 \uD328\uCE58 \uBC29\uC2DD\uC73C\uB85C \uC218\uC815\uD569\uB2C8\uB2E4. PushButton(caption), CheckBox/RadioButton(checked), ComboBox(selected), Edit(text)\uB97C \uC218\uC815\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. kordoc IR\uC5D0 \uC591\uC2DD \uAC1C\uCCB4 \uD0C0\uC785\uC774 \uC5C6\uC73C\uBBC0\uB85C section XML\uC744 \uC9C1\uC811 \uD328\uCE58\uD569\uB2C8\uB2E4. \uC218\uC815 \uC804\uC5D0 list_form_objects\uB85C \uD604\uC7AC \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. name\uC774 \uC911\uBCF5\uB41C \uACBD\uC6B0 index\uB85C \uB300\uC0C1\uC744 \uC9C0\uC815\uD558\uC138\uC694. \uBCC0\uACBD \uC0AC\uD56D\uC740 \uC0AC\uC6A9\uC790 \uC2B9\uC778 \uD6C4\uC5D0\uB9CC \uC800\uC7A5\uB429\uB2C8\uB2E4. .hwpx \uD30C\uC77C \uC804\uC6A9\uC785\uB2C8\uB2E4.",
12711
+ inputSchema: proposeFormObjectSchema,
12712
+ requiresApproval: true,
12713
+ propose: async ({
12714
+ input,
12715
+ ctx
12716
+ }) => {
12717
+ const safePath = await resolveSafePath(ctx.cwd, input.path);
12718
+ const ext = extname5(safePath).toLowerCase();
12719
+ let originalBuffer;
12720
+ try {
12721
+ originalBuffer = await readFile6(safePath);
12722
+ } catch {
12723
+ return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}`;
12724
+ }
12725
+ const bufArray = new Uint8Array(originalBuffer.buffer);
12726
+ const validationError = validateHwpxBuffer(ext, bufArray);
12727
+ if (validationError) return validationError;
12728
+ let zip;
12729
+ try {
12730
+ zip = await import_jszip3.default.loadAsync(bufArray);
12731
+ } catch {
12732
+ return "\uC624\uB958: .hwpx(ZIP) \uD30C\uC77C\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uD30C\uC77C\uC774 \uC190\uC0C1\uB418\uC5C8\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.";
12566
12733
  }
12734
+ const { objects, sectionFiles, sectionXmls } = await listFormObjectsFromZip(zip);
12567
12735
  const resolvedEdits = [];
12568
12736
  const resolveErrors = [];
12569
- for (let i = 0; i < input.edits.length; i++) {
12570
- const e = input.edits[i];
12571
- if (!e) continue;
12572
- if (e.label !== void 0 && (e.row !== void 0 || e.col !== void 0)) {
12737
+ for (let ei = 0; ei < input.edits.length; ei++) {
12738
+ const edit = input.edits[ei];
12739
+ if (!edit) continue;
12740
+ const candidates = objects.filter((o) => o.name === edit.name);
12741
+ if (candidates.length === 0) {
12573
12742
  resolveErrors.push(
12574
- `\uD3B8\uC9D1 #${i + 1}: label\uACFC row/col\uC744 \uB3D9\uC2DC\uC5D0 \uC9C0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC88C\uD45C \uBAA8\uB4DC(tableIndex+row+col) \uB610\uB294 \uB808\uC774\uBE14 \uBAA8\uB4DC(label) \uC911 \uD558\uB098\uB9CC \uC0AC\uC6A9\uD558\uC138\uC694.`
12743
+ `\uD3B8\uC9D1 #${ei + 1}: name="${edit.name}"\uC778 \uC591\uC2DD \uAC1C\uCCB4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. list_form_objects\uB85C \uBB38\uC11C\uC758 \uC591\uC2DD \uAC1C\uCCB4 \uBAA9\uB85D\uC744 \uD655\uC778\uD558\uC138\uC694.`
12575
12744
  );
12576
- } else if (e.label !== void 0) {
12577
- const direction = e.direction ?? "right";
12578
- const resolved = resolveLabelAcrossSections(e.label, direction, e.tableIndex);
12579
- if ("error" in resolved) {
12580
- resolveErrors.push(`\uD3B8\uC9D1 #${i + 1} (\uB808\uC774\uBE14 "${e.label}"): ${resolved.error}`);
12581
- } else {
12582
- resolvedEdits.push({
12583
- tableIndex: resolved.tableIndex,
12584
- row: resolved.row,
12585
- col: resolved.col,
12586
- newText: e.newText,
12587
- expectedText: e.expectedText,
12588
- label: e.label
12589
- });
12745
+ continue;
12746
+ }
12747
+ let target;
12748
+ if (candidates.length > 1) {
12749
+ if (edit.index === void 0) {
12750
+ const indices = candidates.map((c) => c.index).join(", ");
12751
+ resolveErrors.push(
12752
+ `\uD3B8\uC9D1 #${ei + 1}: name="${edit.name}"\uC778 \uC591\uC2DD \uAC1C\uCCB4\uAC00 ${candidates.length}\uAC1C \uBC1C\uACAC\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (\uBB38\uC11C \uC778\uB371\uC2A4: ${indices}). index \uD544\uB4DC\uB85C \uB300\uC0C1\uC744 \uC9C0\uC815\uD558\uC138\uC694.`
12753
+ );
12754
+ continue;
12590
12755
  }
12591
- } else if (e.tableIndex !== void 0 && e.row !== void 0 && e.col !== void 0) {
12592
- resolvedEdits.push({
12593
- tableIndex: e.tableIndex,
12594
- row: e.row,
12595
- col: e.col,
12596
- newText: e.newText,
12597
- expectedText: e.expectedText
12598
- });
12756
+ const byIndex = candidates.find((c) => c.index === edit.index);
12757
+ if (!byIndex) {
12758
+ resolveErrors.push(
12759
+ `\uD3B8\uC9D1 #${ei + 1}: name="${edit.name}", index=${edit.index}\uC778 \uC591\uC2DD \uAC1C\uCCB4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
12760
+ );
12761
+ continue;
12762
+ }
12763
+ target = byIndex;
12599
12764
  } else {
12600
- resolveErrors.push(
12601
- `\uD3B8\uC9D1 #${i + 1}: \uC88C\uD45C \uBAA8\uB4DC(tableIndex+row+col) \uB610\uB294 \uB808\uC774\uBE14 \uBAA8\uB4DC(label) \uC911 \uD558\uB098\uB97C \uC644\uC804\uD788 \uC9C0\uC815\uD558\uC138\uC694. read_document\uB85C \uD45C \uB0B4\uC6A9\uC744 \uBA3C\uC800 \uD655\uC778\uD558\uC138\uC694.`
12602
- );
12765
+ target = candidates[0];
12766
+ if (edit.index !== void 0 && edit.index !== target.index) {
12767
+ resolveErrors.push(
12768
+ `\uD3B8\uC9D1 #${ei + 1}: name="${edit.name}"\uC758 \uBB38\uC11C \uC778\uB371\uC2A4\uB294 ${target.index}\uC774\uC9C0\uB9CC index=${edit.index}\uAC00 \uC9C0\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`
12769
+ );
12770
+ continue;
12771
+ }
12603
12772
  }
12773
+ resolvedEdits.push({
12774
+ target,
12775
+ set: edit.set,
12776
+ expected: edit.expected,
12777
+ editIdx: ei
12778
+ });
12604
12779
  }
12605
12780
  if (resolveErrors.length > 0) {
12606
- return `\uC624\uB958: \uB2E4\uC74C \uB808\uC774\uBE14\uC744 \uD574\uC11D\uD560 \uC218 \uC5C6\uC5B4 \uD30C\uC77C\uC744 \uC218\uC815\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
12781
+ return `\uC624\uB958: \uB2E4\uC74C \uD3B8\uC9D1 \uB300\uC0C1\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC5B4 \uD30C\uC77C\uC744 \uC218\uC815\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
12607
12782
  ${resolveErrors.join("\n")}`;
12608
12783
  }
12609
- const editRequests = resolvedEdits.map((e) => ({
12610
- tableIndex: e.tableIndex,
12611
- row: e.row,
12612
- col: e.col,
12613
- newText: e.newText,
12614
- expectedText: e.expectedText
12615
- }));
12616
- const { buffer: newBuffer, results } = await applyEditsToHwpx(
12617
- new Uint8Array(originalBuffer.buffer),
12618
- editRequests
12619
- );
12620
- const failedResults = results.map((r, i) => ({ r, i })).filter(({ r }) => !r.success);
12621
- if (failedResults.length > 0) {
12622
- const messages = failedResults.map(({ r, i }) => {
12623
- const e = resolvedEdits[i];
12624
- const label = e?.label ? `\uB808\uC774\uBE14 "${e.label}" \u2192 ` : "";
12625
- return `\uD3B8\uC9D1 #${i + 1} (${label}\uD45C ${e?.tableIndex ?? "?"}, \uD589 ${e?.row ?? "?"}, \uC5F4 ${e?.col ?? "?"}): ${r.error}`;
12784
+ const sectionEditMap = /* @__PURE__ */ new Map();
12785
+ for (const re of resolvedEdits) {
12786
+ const si = sectionFiles.indexOf(re.target.sectionFile);
12787
+ if (si < 0) continue;
12788
+ if (!sectionEditMap.has(si)) sectionEditMap.set(si, []);
12789
+ sectionEditMap.get(si).push({
12790
+ target: re.target,
12791
+ set: re.set,
12792
+ expected: re.expected
12626
12793
  });
12794
+ }
12795
+ const sectionResults = [];
12796
+ const newSectionXmls = [...sectionXmls];
12797
+ for (const [si, edits] of sectionEditMap) {
12798
+ const xml = sectionXmls[si] ?? "";
12799
+ const { newXml, results } = applyFormObjectEdits(xml, edits);
12800
+ newSectionXmls[si] = newXml;
12801
+ sectionResults.push({ si, results, edits });
12802
+ }
12803
+ const failMessages = [];
12804
+ const successMap = /* @__PURE__ */ new Map();
12805
+ for (const sr of sectionResults) {
12806
+ for (let i = 0; i < sr.edits.length; i++) {
12807
+ const editReq = sr.edits[i];
12808
+ const result = sr.results[i];
12809
+ const resolved = resolvedEdits.find((r) => r.target === editReq.target);
12810
+ if (resolved) {
12811
+ successMap.set(resolved.editIdx, result);
12812
+ if (!result.success) {
12813
+ failMessages.push(
12814
+ `\uD3B8\uC9D1 #${resolved.editIdx + 1} (name="${editReq.target.name}"): ${result.error}`
12815
+ );
12816
+ }
12817
+ }
12818
+ }
12819
+ }
12820
+ if (failMessages.length > 0) {
12627
12821
  return `\uC624\uB958: \uB2E4\uC74C \uD3B8\uC9D1\uC744 \uC801\uC6A9\uD560 \uC218 \uC5C6\uC5B4 \uD30C\uC77C\uC744 \uC218\uC815\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
12628
- ${messages.join("\n")}`;
12822
+ ${failMessages.join("\n")}`;
12629
12823
  }
12630
- const diffLines = ["| \uD45C\xB7\uC140 | \uC774\uC804 | \uC774\uD6C4 |", "| --- | --- | --- |"];
12631
- for (let i = 0; i < resolvedEdits.length; i++) {
12632
- const e = resolvedEdits[i];
12633
- if (!e) continue;
12634
- const oldText = results[i]?.oldText ?? "";
12635
- const addr = e.label ? `\uB808\uC774\uBE14 "${e.label}" \u2192 #${e.tableIndex} (${e.row},${e.col})` : `#${e.tableIndex} (${e.row},${e.col})`;
12636
- diffLines.push(`| ${addr} | ${oldText} | ${e.newText} |`);
12824
+ const out = new import_jszip3.default();
12825
+ const mimetypeEntry = zip.file("mimetype");
12826
+ if (mimetypeEntry) {
12827
+ out.file("mimetype", await mimetypeEntry.async("uint8array"), { compression: "STORE" });
12828
+ }
12829
+ for (const [name, entry] of Object.entries(zip.files)) {
12830
+ if (name === "mimetype" || entry.dir) continue;
12831
+ const si = sectionFiles.indexOf(name);
12832
+ if (si >= 0) {
12833
+ out.file(name, newSectionXmls[si] ?? "");
12834
+ } else {
12835
+ out.file(name, await entry.async("uint8array"));
12836
+ }
12837
+ }
12838
+ const buf = await out.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
12839
+ const newBuffer = new Uint8Array(buf);
12840
+ const typeKo = {
12841
+ button: "PushButton",
12842
+ checkBox: "CheckBox",
12843
+ radioButton: "RadioButton",
12844
+ comboBox: "ComboBox",
12845
+ edit: "Edit"
12846
+ };
12847
+ const diffLines = ["| \uC591\uC2DD \uAC1C\uCCB4 | \uC774\uC804 | \uC774\uD6C4 |", "| --- | --- | --- |"];
12848
+ for (const re of resolvedEdits) {
12849
+ const result = successMap.get(re.editIdx);
12850
+ const oldVal = result?.oldValue ?? "";
12851
+ const oldStr = typeof oldVal === "boolean" ? oldVal ? "CHECKED" : "UNCHECKED" : String(oldVal);
12852
+ let newStr;
12853
+ if (re.set.caption !== void 0) newStr = re.set.caption;
12854
+ else if (re.set.checked !== void 0) newStr = re.set.checked ? "CHECKED" : "UNCHECKED";
12855
+ else if (re.set.selected !== void 0) newStr = re.set.selected;
12856
+ else newStr = re.set.text ?? "";
12857
+ diffLines.push(`| ${re.target.name}(${typeKo[re.target.type]}) | ${oldStr} | ${newStr} |`);
12637
12858
  }
12638
12859
  const diff = diffLines.join("\n");
12639
12860
  const { outputPath, willConvertFormat } = resolveOutputPath(safePath);
@@ -12642,7 +12863,7 @@ ${messages.join("\n")}`;
12642
12863
  return {
12643
12864
  proposal: {
12644
12865
  id: proposalId,
12645
- kind: "cell-edit",
12866
+ kind: "form-object",
12646
12867
  targetPath: outputPath,
12647
12868
  stagedPath,
12648
12869
  summary: input.summary,
@@ -12659,6 +12880,92 @@ ${messages.join("\n")}`;
12659
12880
  };
12660
12881
  }
12661
12882
  };
12883
+ var DOC_EXTENSIONS = /* @__PURE__ */ new Set([
12884
+ ".hwp",
12885
+ ".hwpx",
12886
+ ".hwpml",
12887
+ ".docx",
12888
+ ".doc",
12889
+ ".xlsx",
12890
+ ".xls",
12891
+ ".pdf",
12892
+ ".pptx",
12893
+ ".ppt",
12894
+ ".md",
12895
+ ".txt"
12896
+ ]);
12897
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
12898
+ "node_modules",
12899
+ ".git",
12900
+ ".DS_Store",
12901
+ "__pycache__",
12902
+ "dist",
12903
+ "build",
12904
+ ".next"
12905
+ ]);
12906
+ var MAX_DEPTH = 4;
12907
+ var MAX_FILES = 500;
12908
+ var listFilesSchema = z6.object({
12909
+ dir: z6.string().optional().describe("\uBAA9\uB85D\uC744 \uC870\uD68C\uD560 \uB514\uB809\uD130\uB9AC (\uBBF8\uC9C0\uC815 \uC2DC cwd \uC804\uCCB4)")
12910
+ });
12911
+ async function collectFiles(dir, cwd, depth, entries) {
12912
+ if (depth > MAX_DEPTH || entries.length >= MAX_FILES) return;
12913
+ let items;
12914
+ try {
12915
+ items = await readdir3(dir);
12916
+ } catch {
12917
+ return;
12918
+ }
12919
+ for (const item of items) {
12920
+ if (entries.length >= MAX_FILES) break;
12921
+ if (item.startsWith(".")) continue;
12922
+ if (SKIP_DIRS.has(item)) continue;
12923
+ const fullPath = join4(dir, item);
12924
+ let info;
12925
+ try {
12926
+ info = await stat3(fullPath);
12927
+ } catch {
12928
+ continue;
12929
+ }
12930
+ const relPath = relative2(cwd, fullPath);
12931
+ const ext = extname6(item).toLowerCase();
12932
+ const isDoc = DOC_EXTENSIONS.has(ext);
12933
+ if (info.isDirectory()) {
12934
+ entries.push({ path: relPath + "/", isDir: true, isDoc: false });
12935
+ await collectFiles(fullPath, cwd, depth + 1, entries);
12936
+ } else if (info.isFile()) {
12937
+ entries.push({ path: relPath, isDir: false, isDoc });
12938
+ }
12939
+ }
12940
+ }
12941
+ var listFilesTool = {
12942
+ name: "list_files",
12943
+ description: "\uD604\uC7AC \uC791\uC5C5 \uB514\uB809\uD130\uB9AC(cwd) \uC774\uD558\uC758 \uD30C\uC77C \uBAA9\uB85D\uC744 \uBC18\uD658\uD569\uB2C8\uB2E4. \uBB38\uC11C \uD30C\uC77C(.hwp/.hwpx/.docx/.xlsx/.pdf \uB4F1)\uC774 \uCD5C\uC0C1\uB2E8\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4. \uAE4A\uC774\uB294 \uCD5C\uB300 4\uB2E8\uACC4\uAE4C\uC9C0\uC785\uB2C8\uB2E4.",
12944
+ inputSchema: listFilesSchema,
12945
+ requiresApproval: false,
12946
+ execute: async ({
12947
+ input,
12948
+ ctx
12949
+ }) => {
12950
+ const targetDir = input.dir ? await resolveSafePath(ctx.cwd, input.dir) : ctx.cwd;
12951
+ const entries = [];
12952
+ await collectFiles(targetDir, ctx.cwd, 0, entries);
12953
+ if (entries.length === 0) {
12954
+ return "\uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.";
12955
+ }
12956
+ const docs = entries.filter((e) => e.isDoc);
12957
+ const dirs = entries.filter((e) => e.isDir);
12958
+ const others = entries.filter((e) => !e.isDoc && !e.isDir);
12959
+ const allSorted = [...docs, ...dirs, ...others];
12960
+ const lines = allSorted.map((e) => {
12961
+ const icon = e.isDir ? "\u{1F4C1}" : e.isDoc ? "\u{1F4C4}" : " ";
12962
+ return `${icon} ${e.path}`;
12963
+ });
12964
+ const truncateNotice = entries.length >= MAX_FILES ? `
12965
+ (\uCD5C\uB300 ${MAX_FILES}\uAC1C\uAE4C\uC9C0 \uD45C\uC2DC\uB429\uB2C8\uB2E4. \uB354 \uC881\uC740 \uBC94\uC704\uB97C \uC9C0\uC815\uD558\uC138\uC694.)` : "";
12966
+ return lines.join("\n") + truncateNotice;
12967
+ }
12968
+ };
12662
12969
  var HEADING_LEVELS = {
12663
12970
  1: HeadingLevel.HEADING_1,
12664
12971
  2: HeadingLevel.HEADING_2,
@@ -12781,10 +13088,10 @@ async function markdownToDocx(markdown) {
12781
13088
  });
12782
13089
  return Packer.toBuffer(doc);
12783
13090
  }
12784
- var proposeEditSchema = z6.object({
12785
- path: z6.string().describe("\uC218\uC815\uD560 \uBB38\uC11C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
12786
- newMarkdown: z6.string().describe("\uC0C8 \uBB38\uC11C \uB0B4\uC6A9 (\uB9C8\uD06C\uB2E4\uC6B4 \uD615\uC2DD). read_document\uB85C \uC6D0\uBCF8\uC744 \uBA3C\uC800 \uC77D\uC5B4\uC57C \uD568"),
12787
- summary: z6.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
13091
+ var proposeEditSchema = z7.object({
13092
+ path: z7.string().describe("\uC218\uC815\uD560 \uBB38\uC11C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
13093
+ newMarkdown: z7.string().describe("\uC0C8 \uBB38\uC11C \uB0B4\uC6A9 (\uB9C8\uD06C\uB2E4\uC6B4 \uD615\uC2DD). read_document\uB85C \uC6D0\uBCF8\uC744 \uBA3C\uC800 \uC77D\uC5B4\uC57C \uD568"),
13094
+ summary: z7.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
12788
13095
  });
12789
13096
  var proposeEditTool = {
12790
13097
  name: "propose_edit",
@@ -12796,10 +13103,10 @@ var proposeEditTool = {
12796
13103
  ctx
12797
13104
  }) => {
12798
13105
  const safePath = await resolveSafePath(ctx.cwd, input.path);
12799
- const ext = extname6(safePath).toLowerCase();
13106
+ const ext = extname7(safePath).toLowerCase();
12800
13107
  let originalBuffer;
12801
13108
  try {
12802
- originalBuffer = await readFile6(safePath);
13109
+ originalBuffer = await readFile7(safePath);
12803
13110
  } catch {
12804
13111
  return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}. \uACBD\uB85C\uB97C \uD655\uC778\uD558\uAC70\uB098 read_document\uB85C \uBA3C\uC800 \uD655\uC778\uD558\uC138\uC694.`;
12805
13112
  }
@@ -12870,19 +13177,19 @@ ${diff}`;
12870
13177
  };
12871
13178
  }
12872
13179
  };
12873
- var proposeFindReplaceSchema = z7.object({
12874
- path: z7.string().describe("\uC218\uC815\uD560 .hwpx \uD30C\uC77C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
12875
- find: z7.string().min(1).describe("\uCC3E\uC744 \uD14D\uC2A4\uD2B8"),
12876
- replace: z7.string().describe("\uBC14\uAFC0 \uD14D\uC2A4\uD2B8"),
12877
- caseSensitive: z7.boolean().optional().default(false).describe("\uB300\uC18C\uBB38\uC790 \uAD6C\uBD84 (\uAE30\uBCF8\uAC12: false)"),
12878
- all: z7.boolean().optional().default(true).describe("\uBAA8\uB4E0 \uD56D\uBAA9\uC744 \uAD50\uCCB4\uD560\uC9C0 \uC5EC\uBD80 (\uAE30\uBCF8\uAC12: true). false\uC774\uBA74 \uCCAB \uBC88\uC9F8 \uB9E4\uCE58\uB9CC \uAD50\uCCB4"),
12879
- summary: z7.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
13180
+ var proposeFindReplaceSchema = z8.object({
13181
+ path: z8.string().describe("\uC218\uC815\uD560 .hwpx \uD30C\uC77C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
13182
+ find: z8.string().min(1).describe("\uCC3E\uC744 \uD14D\uC2A4\uD2B8"),
13183
+ replace: z8.string().describe("\uBC14\uAFC0 \uD14D\uC2A4\uD2B8"),
13184
+ caseSensitive: z8.boolean().optional().default(false).describe("\uB300\uC18C\uBB38\uC790 \uAD6C\uBD84 (\uAE30\uBCF8\uAC12: false)"),
13185
+ all: z8.boolean().optional().default(true).describe("\uBAA8\uB4E0 \uD56D\uBAA9\uC744 \uAD50\uCCB4\uD560\uC9C0 \uC5EC\uBD80 (\uAE30\uBCF8\uAC12: true). false\uC774\uBA74 \uCCAB \uBC88\uC9F8 \uB9E4\uCE58\uB9CC \uAD50\uCCB4"),
13186
+ summary: z8.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
12880
13187
  });
12881
13188
  var MAX_DIFF_SAMPLES = 20;
12882
13189
  function escapeXml3(text3) {
12883
13190
  return text3.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
12884
13191
  }
12885
- function unescapeXml(text3) {
13192
+ function unescapeXml2(text3) {
12886
13193
  return text3.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
12887
13194
  }
12888
13195
  function makeChangeSnippet(before, after, ctx = 24) {
@@ -12906,7 +13213,7 @@ function collectChangedSnippets(beforeXml, afterXml, maxSamples) {
12906
13213
  const n = Math.min(beforeNodes.length, afterNodes.length);
12907
13214
  for (let i = 0; i < n && out.length < maxSamples; i++) {
12908
13215
  if (beforeNodes[i] !== afterNodes[i]) {
12909
- const snip = makeChangeSnippet(unescapeXml(beforeNodes[i]), unescapeXml(afterNodes[i]));
13216
+ const snip = makeChangeSnippet(unescapeXml2(beforeNodes[i]), unescapeXml2(afterNodes[i]));
12910
13217
  out.push(snip);
12911
13218
  }
12912
13219
  }
@@ -12989,7 +13296,7 @@ function countOccurrences(str, sub) {
12989
13296
  return count;
12990
13297
  }
12991
13298
  async function applyFindReplaceToHwpx(hwpxBuffer, find, replace, caseSensitive, replaceAll) {
12992
- const zip = await import_jszip3.default.loadAsync(hwpxBuffer);
13299
+ const zip = await import_jszip4.default.loadAsync(hwpxBuffer);
12993
13300
  const sectionFiles = Object.keys(zip.files).filter((name) => /^Contents\/section\d+\.xml$/.test(name)).sort();
12994
13301
  const sectionXmls = [];
12995
13302
  for (const sf of sectionFiles) {
@@ -13033,7 +13340,7 @@ async function applyFindReplaceToHwpx(hwpxBuffer, find, replace, caseSensitive,
13033
13340
  samples.push(snip);
13034
13341
  }
13035
13342
  }
13036
- const out = new import_jszip3.default();
13343
+ const out = new import_jszip4.default();
13037
13344
  const mimetypeEntry = zip.file("mimetype");
13038
13345
  if (mimetypeEntry) {
13039
13346
  out.file("mimetype", await mimetypeEntry.async("uint8array"), { compression: "STORE" });
@@ -13064,7 +13371,7 @@ var proposeFindReplaceTool = {
13064
13371
  ctx
13065
13372
  }) => {
13066
13373
  const safePath = await resolveSafePath(ctx.cwd, input.path);
13067
- const ext = extname7(safePath).toLowerCase();
13374
+ const ext = extname8(safePath).toLowerCase();
13068
13375
  if (ext === ".hwp") {
13069
13376
  return "\uC624\uB958: propose_find_replace\uB294 .hwpx \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. .hwp(\uAD6C\uD615 OLE \uBC14\uC774\uB108\uB9AC)\uB294 XML \uC9C1\uC811 \uD3B8\uC9D1\uC774 \uBD88\uAC00\uD569\uB2C8\uB2E4. \uD55C\uAE00 \uD504\uB85C\uADF8\uB7A8\uC5D0\uC11C '\uB2E4\uB978 \uC774\uB984\uC73C\uB85C \uC800\uC7A5 \u2192 .hwpx'\uB85C \uC800\uC7A5\uD55C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.";
13070
13377
  }
@@ -13073,7 +13380,7 @@ var proposeFindReplaceTool = {
13073
13380
  }
13074
13381
  let originalBuf;
13075
13382
  try {
13076
- originalBuf = await readFile7(safePath);
13383
+ originalBuf = await readFile8(safePath);
13077
13384
  } catch {
13078
13385
  return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}. \uACBD\uB85C\uB97C \uD655\uC778\uD558\uC138\uC694.`;
13079
13386
  }
@@ -13166,10 +13473,10 @@ var proposeFindReplaceTool = {
13166
13473
  };
13167
13474
  }
13168
13475
  };
13169
- var proposeFormFillSchema = z8.object({
13170
- path: z8.string().describe("\uC591\uC2DD \uBB38\uC11C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
13171
- fields: z8.record(z8.string(), z8.string()).describe("\uCC44\uC6B8 \uD544\uB4DC \uB9E4\uD551: { \uB77C\uBCA8: \uAC12 }. read_document\uB85C \uBA3C\uC800 \uD544\uB4DC \uBAA9\uB85D\uC744 \uD655\uC778\uD558\uC138\uC694"),
13172
- summary: z8.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
13476
+ var proposeFormFillSchema = z9.object({
13477
+ path: z9.string().describe("\uC591\uC2DD \uBB38\uC11C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
13478
+ fields: z9.record(z9.string(), z9.string()).describe("\uCC44\uC6B8 \uD544\uB4DC \uB9E4\uD551: { \uB77C\uBCA8: \uAC12 }. read_document\uB85C \uBA3C\uC800 \uD544\uB4DC \uBAA9\uB85D\uC744 \uD655\uC778\uD558\uC138\uC694"),
13479
+ summary: z9.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
13173
13480
  });
13174
13481
  var proposeFormFillTool = {
13175
13482
  name: "propose_form_fill",
@@ -13181,13 +13488,13 @@ var proposeFormFillTool = {
13181
13488
  ctx
13182
13489
  }) => {
13183
13490
  const safePath = await resolveSafePath(ctx.cwd, input.path);
13184
- const ext = extname8(safePath).toLowerCase();
13491
+ const ext = extname9(safePath).toLowerCase();
13185
13492
  if (ext !== ".hwpx" && ext !== ".hwp") {
13186
13493
  return `\uC624\uB958: propose_form_fill\uC740 .hwp/.hwpx \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. \uD604\uC7AC \uD30C\uC77C: ${ext}. .docx \uD30C\uC77C\uC740 propose_edit\uC744 \uC0AC\uC6A9\uD558\uC138\uC694.`;
13187
13494
  }
13188
13495
  let originalBuffer;
13189
13496
  try {
13190
- originalBuffer = await readFile8(safePath);
13497
+ originalBuffer = await readFile9(safePath);
13191
13498
  } catch {
13192
13499
  return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}. \uACBD\uB85C\uB97C \uD655\uC778\uD558\uC138\uC694.`;
13193
13500
  }
@@ -13255,16 +13562,216 @@ var proposeFormFillTool = {
13255
13562
  };
13256
13563
  }
13257
13564
  };
13258
- var proposeSheetEditSchema = z9.object({
13259
- path: z9.string().describe("\uC218\uC815\uD560 XLSX \uD30C\uC77C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
13260
- updates: z9.array(
13261
- z9.object({
13262
- sheet: z9.string().describe("\uC2DC\uD2B8 \uC774\uB984"),
13263
- cell: z9.string().describe("\uC140 \uC8FC\uC18C (\uC608: A1, B3)"),
13264
- value: z9.union([z9.string(), z9.number()]).describe("\uC0C8 \uAC12")
13565
+ var proposeRedactPiiSchema = z10.object({
13566
+ path: z10.string().describe("\uAC1C\uC778\uC815\uBCF4\uB97C \uBE44\uC2DD\uBCC4 \uCC98\uB9AC\uD560 \uBB38\uC11C \uACBD\uB85C"),
13567
+ summary: z10.string().optional().describe("\uBCC0\uACBD \uC694\uC57D")
13568
+ });
13569
+ function mergeFindings(allFindings) {
13570
+ const map = /* @__PURE__ */ new Map();
13571
+ for (const findings of allFindings) {
13572
+ for (const f of findings) {
13573
+ const existing = map.get(f.type);
13574
+ if (existing) {
13575
+ existing.count += f.count;
13576
+ for (const m of f.masked) {
13577
+ if (!existing.masked.includes(m) && existing.masked.length < 5) {
13578
+ existing.masked.push(m);
13579
+ }
13580
+ }
13581
+ } else {
13582
+ map.set(f.type, { type: f.type, count: f.count, masked: [...f.masked] });
13583
+ }
13584
+ }
13585
+ }
13586
+ return [...map.values()];
13587
+ }
13588
+ async function applyRedactToHwpx(hwpxBuffer) {
13589
+ const zip = await import_jszip5.default.loadAsync(hwpxBuffer);
13590
+ const sectionFiles = Object.keys(zip.files).filter((name) => /^Contents\/section\d+\.xml$/.test(name)).sort();
13591
+ const sectionXmls = [];
13592
+ for (const sf of sectionFiles) {
13593
+ const entry = zip.file(sf);
13594
+ const xml = entry ? await entry.async("string") : "";
13595
+ sectionXmls.push(xml);
13596
+ }
13597
+ const newSectionXmls = [];
13598
+ const allFindings = [];
13599
+ let anyChanged = false;
13600
+ for (const srcXml of sectionXmls) {
13601
+ const tNodeRe = /<hp:t>([\s\S]*?)<\/hp:t>/g;
13602
+ const sectionFindings = [];
13603
+ let offset = 0;
13604
+ let result = srcXml;
13605
+ let m = tNodeRe.exec(srcXml);
13606
+ while (m !== null) {
13607
+ const content = m[1];
13608
+ if (content.length === 0) {
13609
+ m = tNodeRe.exec(srcXml);
13610
+ continue;
13611
+ }
13612
+ const { text: redacted, findings } = redactText(content);
13613
+ if (redacted !== content) {
13614
+ const openTagLen = "<hp:t>".length;
13615
+ const contentStart = m.index + offset + openTagLen;
13616
+ const contentEnd = contentStart + content.length;
13617
+ result = result.substring(0, contentStart) + redacted + result.substring(contentEnd);
13618
+ offset += redacted.length - content.length;
13619
+ anyChanged = true;
13620
+ }
13621
+ if (findings.length > 0) {
13622
+ sectionFindings.push(findings);
13623
+ }
13624
+ m = tNodeRe.exec(srcXml);
13625
+ }
13626
+ newSectionXmls.push(result);
13627
+ allFindings.push(...sectionFindings);
13628
+ }
13629
+ if (!anyChanged) {
13630
+ return { buffer: hwpxBuffer, findings: mergeFindings(allFindings), changed: false };
13631
+ }
13632
+ const out = new import_jszip5.default();
13633
+ const mimetypeEntry = zip.file("mimetype");
13634
+ if (mimetypeEntry) {
13635
+ out.file("mimetype", await mimetypeEntry.async("uint8array"), { compression: "STORE" });
13636
+ }
13637
+ for (const [name, entry] of Object.entries(zip.files)) {
13638
+ if (name === "mimetype" || entry.dir) continue;
13639
+ const sectionIdx = sectionFiles.indexOf(name);
13640
+ if (sectionIdx >= 0) {
13641
+ out.file(name, newSectionXmls[sectionIdx] ?? "");
13642
+ } else {
13643
+ out.file(name, await entry.async("uint8array"));
13644
+ }
13645
+ }
13646
+ const buf = await out.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
13647
+ return {
13648
+ buffer: new Uint8Array(buf),
13649
+ findings: mergeFindings(allFindings),
13650
+ changed: true
13651
+ };
13652
+ }
13653
+ function buildDiff(findings) {
13654
+ const total = findings.reduce((s, f) => s + f.count, 0);
13655
+ const lines = [`\uAC1C\uC778\uC815\uBCF4 ${total}\uAC74 \uBE44\uC2DD\uBCC4 \uCC98\uB9AC`];
13656
+ for (const f of findings) {
13657
+ const examples = f.masked.slice(0, 3).join(", ");
13658
+ lines.push(`- ${f.type}: ${f.count}\uAC74 \u2192 ${examples}`);
13659
+ }
13660
+ return lines.join("\n");
13661
+ }
13662
+ var proposeRedactPiiTool = {
13663
+ name: "propose_redact_pii",
13664
+ description: "\uBB38\uC11C\uC758 \uAC1C\uC778\uC815\uBCF4(\uC8FC\uBBFC\uB4F1\uB85D\uBC88\uD638\xB7\uC804\uD654\uBC88\uD638\xB7\uC774\uBA54\uC77C\xB7\uC2E0\uC6A9\uCE74\uB4DC\uBC88\uD638)\uB97C \uAC00\uB9AC\uAE30/\uB9C8\uC2A4\uD0B9/\uBE44\uC2DD\uBCC4 \uCC98\uB9AC\uD569\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uAC00 '\uAC1C\uC778\uC815\uBCF4 \uAC00\uB824\uC918/\uC9C0\uC6CC\uC918/\uBE44\uC2DD\uBCC4 \uCC98\uB9AC\uD574\uC918/\uB9C8\uC2A4\uD0B9\uD574\uC918'\uB77C\uACE0 \uD558\uBA74 \uC774 \uB3C4\uAD6C\uB85C \uC218\uC815\uC548\uC744 \uC81C\uC548\uD558\uC138\uC694(scan_pii\uB294 \uD655\uC778\uC6A9\uC77C \uBFD0 \uC218\uC815\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4). .hwpx\uB294 XML \uC9C1\uC811 \uD328\uCE58\uB85C \uAD6C\uC870(\uD45C\xB7\uC774\uBBF8\uC9C0\xB7\uC11C\uC2DD)\uB97C \uBCF4\uC874\uD558\uBA70, .md/.txt\uB3C4 \uC9C0\uC6D0\uD569\uB2C8\uB2E4. \uBCC0\uACBD\uC740 \uC2B9\uC778 \uD6C4\uC5D0\uB9CC \uC800\uC7A5\uB429\uB2C8\uB2E4. \uC6D0\uBB38 \uAC12\uC740 \uD45C\uC2DC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
13665
+ inputSchema: proposeRedactPiiSchema,
13666
+ requiresApproval: true,
13667
+ propose: async ({
13668
+ input,
13669
+ ctx
13670
+ }) => {
13671
+ const safePath = await resolveSafePath(ctx.cwd, input.path);
13672
+ const ext = extname10(safePath).toLowerCase();
13673
+ if (ext === ".hwp") {
13674
+ return "\uC624\uB958: propose_redact_pii\uB294 .hwpx \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. .hwp(\uAD6C\uD615 OLE \uBC14\uC774\uB108\uB9AC)\uB294 XML \uC9C1\uC811 \uD3B8\uC9D1\uC774 \uBD88\uAC00\uD569\uB2C8\uB2E4. \uD55C\uAE00 \uD504\uB85C\uADF8\uB7A8\uC5D0\uC11C '\uB2E4\uB978 \uC774\uB984\uC73C\uB85C \uC800\uC7A5 \u2192 .hwpx'\uB85C \uC800\uC7A5\uD55C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.";
13675
+ }
13676
+ if (ext !== ".hwpx" && ext !== ".md" && ext !== ".txt") {
13677
+ const hint = ext === ".docx" || ext === ".xlsx" ? " .hwpx/.md/.txt\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4." : " .hwpx/.md/.txt\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4.";
13678
+ return `\uC624\uB958: propose_redact_pii\uB294 .hwpx/.md/.txt \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. \uD604\uC7AC \uD30C\uC77C \uD655\uC7A5\uC790: ${ext}.${hint}`;
13679
+ }
13680
+ const { outputPath, willConvertFormat } = resolveOutputPath(safePath);
13681
+ if (ext === ".hwpx") {
13682
+ let originalBuf;
13683
+ try {
13684
+ originalBuf = await readFile10(safePath);
13685
+ } catch {
13686
+ return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}`;
13687
+ }
13688
+ if (originalBuf[0] !== 80 || originalBuf[1] !== 75) {
13689
+ return "\uC624\uB958: \uD30C\uC77C\uC774 \uC720\uD6A8\uD55C .hwpx(ZIP) \uD3EC\uB9F7\uC774 \uC544\uB2D9\uB2C8\uB2E4. \uD30C\uC77C\uC774 \uC190\uC0C1\uB418\uC5C8\uAC70\uB098 \uAD6C\uD615 .hwp(OLE \uBC14\uC774\uB108\uB9AC) \uD3EC\uB9F7\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4.";
13690
+ }
13691
+ const originalBytes = new Uint8Array(
13692
+ originalBuf.buffer,
13693
+ originalBuf.byteOffset,
13694
+ originalBuf.byteLength
13695
+ );
13696
+ let patchResult;
13697
+ try {
13698
+ patchResult = await applyRedactToHwpx(originalBytes);
13699
+ } catch (e) {
13700
+ return `\uC624\uB958: \uBE44\uC2DD\uBCC4 \uCC98\uB9AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. ${String(e)}`;
13701
+ }
13702
+ const { buffer: newBytes, findings: findings2, changed } = patchResult;
13703
+ const totalCount2 = findings2.reduce((s, f) => s + f.count, 0);
13704
+ if (!changed || totalCount2 === 0) {
13705
+ return `\uAC1C\uC778\uC815\uBCF4\uAC00 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC544 \uBCC0\uACBD\uD560 \uB0B4\uC6A9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}`;
13706
+ }
13707
+ const stagedPath2 = await stageFile(ctx.sessionId, outputPath, newBytes);
13708
+ const proposalId2 = crypto.randomUUID();
13709
+ const diff2 = buildDiff(findings2);
13710
+ const summaryText2 = input.summary ?? `\uAC1C\uC778\uC815\uBCF4 \uBE44\uC2DD\uBCC4 \uCC98\uB9AC(\uB9C8\uC2A4\uD0B9): ${basename4(safePath)}`;
13711
+ return {
13712
+ proposal: {
13713
+ id: proposalId2,
13714
+ kind: "redact-pii",
13715
+ targetPath: outputPath,
13716
+ stagedPath: stagedPath2,
13717
+ summary: summaryText2,
13718
+ diff: diff2,
13719
+ warnings: [],
13720
+ willConvertFormat
13721
+ },
13722
+ commit: async () => {
13723
+ const backupPath = await backupFile(outputPath);
13724
+ await commitStaged(stagedPath2, outputPath);
13725
+ const backupInfo = backupPath ? ` (\uBC31\uC5C5: ${backupPath})` : "";
13726
+ return `\uAC1C\uC778\uC815\uBCF4 \uBE44\uC2DD\uBCC4 \uC644\uB8CC: ${outputPath}${backupInfo}`;
13727
+ }
13728
+ };
13729
+ }
13730
+ let originalText;
13731
+ try {
13732
+ originalText = await readFile10(safePath, "utf-8");
13733
+ } catch {
13734
+ return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}`;
13735
+ }
13736
+ const { text: redacted, findings } = redactText(originalText);
13737
+ const totalCount = findings.reduce((s, f) => s + f.count, 0);
13738
+ if (totalCount === 0) {
13739
+ return `\uAC1C\uC778\uC815\uBCF4\uAC00 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC544 \uBCC0\uACBD\uD560 \uB0B4\uC6A9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}`;
13740
+ }
13741
+ const stagedPath = await stageFile(ctx.sessionId, outputPath, redacted);
13742
+ const proposalId = crypto.randomUUID();
13743
+ const diff = buildDiff(findings);
13744
+ const summaryText = input.summary ?? `\uAC1C\uC778\uC815\uBCF4 \uBE44\uC2DD\uBCC4 \uCC98\uB9AC(\uB9C8\uC2A4\uD0B9): ${basename4(safePath)}`;
13745
+ return {
13746
+ proposal: {
13747
+ id: proposalId,
13748
+ kind: "redact-pii",
13749
+ targetPath: outputPath,
13750
+ stagedPath,
13751
+ summary: summaryText,
13752
+ diff,
13753
+ warnings: [],
13754
+ willConvertFormat
13755
+ },
13756
+ commit: async () => {
13757
+ const backupPath = await backupFile(outputPath);
13758
+ await commitStaged(stagedPath, outputPath);
13759
+ const backupInfo = backupPath ? ` (\uBC31\uC5C5: ${backupPath})` : "";
13760
+ return `\uAC1C\uC778\uC815\uBCF4 \uBE44\uC2DD\uBCC4 \uC644\uB8CC: ${outputPath}${backupInfo}`;
13761
+ }
13762
+ };
13763
+ }
13764
+ };
13765
+ var proposeSheetEditSchema = z11.object({
13766
+ path: z11.string().describe("\uC218\uC815\uD560 XLSX \uD30C\uC77C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
13767
+ updates: z11.array(
13768
+ z11.object({
13769
+ sheet: z11.string().describe("\uC2DC\uD2B8 \uC774\uB984"),
13770
+ cell: z11.string().describe("\uC140 \uC8FC\uC18C (\uC608: A1, B3)"),
13771
+ value: z11.union([z11.string(), z11.number()]).describe("\uC0C8 \uAC12")
13265
13772
  })
13266
13773
  ).min(1).describe("\uC218\uC815\uD560 \uC140 \uBAA9\uB85D. read_document\uB85C \uC6D0\uBCF8 \uC2DC\uD2B8 \uAD6C\uC870\uB97C \uBA3C\uC800 \uD655\uC778\uD558\uC138\uC694"),
13267
- summary: z9.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
13774
+ summary: z11.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
13268
13775
  });
13269
13776
  var proposeSheetEditTool = {
13270
13777
  name: "propose_sheet_edit",
@@ -13276,13 +13783,13 @@ var proposeSheetEditTool = {
13276
13783
  ctx
13277
13784
  }) => {
13278
13785
  const safePath = await resolveSafePath(ctx.cwd, input.path);
13279
- const ext = extname9(safePath).toLowerCase();
13786
+ const ext = extname11(safePath).toLowerCase();
13280
13787
  if (ext !== ".xlsx" && ext !== ".xls") {
13281
13788
  return `\uC624\uB958: propose_sheet_edit\uC740 .xlsx/.xls \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. \uD604\uC7AC \uD30C\uC77C: ${ext}.`;
13282
13789
  }
13283
13790
  let originalBuffer;
13284
13791
  try {
13285
- originalBuffer = await readFile9(safePath);
13792
+ originalBuffer = await readFile11(safePath);
13286
13793
  } catch {
13287
13794
  return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}. \uACBD\uB85C\uB97C \uD655\uC778\uD558\uC138\uC694.`;
13288
13795
  }
@@ -13375,47 +13882,47 @@ function detectStructuralLoss(beforeBlocks, afterBlocks) {
13375
13882
  }
13376
13883
  return { lost: false, detail: "" };
13377
13884
  }
13378
- var insertRowOpSchema = z10.object({
13379
- type: z10.literal("insertRow"),
13380
- row: z10.number().int().nonnegative().describe("\uAE30\uC900 \uD589 \uC778\uB371\uC2A4 (0-based)"),
13381
- position: z10.enum(["above", "below"]).describe("\uC0BD\uC785 \uC704\uCE58: above=row \uC704\uC5D0, below=row \uC544\uB798\uC5D0")
13885
+ var insertRowOpSchema = z12.object({
13886
+ type: z12.literal("insertRow"),
13887
+ row: z12.number().int().nonnegative().describe("\uAE30\uC900 \uD589 \uC778\uB371\uC2A4 (0-based)"),
13888
+ position: z12.enum(["above", "below"]).describe("\uC0BD\uC785 \uC704\uCE58: above=row \uC704\uC5D0, below=row \uC544\uB798\uC5D0")
13382
13889
  });
13383
- var deleteRowOpSchema = z10.object({
13384
- type: z10.literal("deleteRow"),
13385
- row: z10.number().int().nonnegative().describe("\uC0AD\uC81C\uD560 \uD589 \uC778\uB371\uC2A4 (0-based)")
13890
+ var deleteRowOpSchema = z12.object({
13891
+ type: z12.literal("deleteRow"),
13892
+ row: z12.number().int().nonnegative().describe("\uC0AD\uC81C\uD560 \uD589 \uC778\uB371\uC2A4 (0-based)")
13386
13893
  });
13387
- var insertColumnOpSchema = z10.object({
13388
- type: z10.literal("insertColumn"),
13389
- col: z10.number().int().nonnegative().describe("\uAE30\uC900 \uC5F4 \uC778\uB371\uC2A4 (0-based)"),
13390
- position: z10.enum(["left", "right"]).describe("\uC0BD\uC785 \uC704\uCE58: left=col \uC67C\uCABD\uC5D0, right=col \uC624\uB978\uCABD\uC5D0")
13894
+ var insertColumnOpSchema = z12.object({
13895
+ type: z12.literal("insertColumn"),
13896
+ col: z12.number().int().nonnegative().describe("\uAE30\uC900 \uC5F4 \uC778\uB371\uC2A4 (0-based)"),
13897
+ position: z12.enum(["left", "right"]).describe("\uC0BD\uC785 \uC704\uCE58: left=col \uC67C\uCABD\uC5D0, right=col \uC624\uB978\uCABD\uC5D0")
13391
13898
  });
13392
- var deleteColumnOpSchema = z10.object({
13393
- type: z10.literal("deleteColumn"),
13394
- col: z10.number().int().nonnegative().describe("\uC0AD\uC81C\uD560 \uC5F4 \uC778\uB371\uC2A4 (0-based)")
13899
+ var deleteColumnOpSchema = z12.object({
13900
+ type: z12.literal("deleteColumn"),
13901
+ col: z12.number().int().nonnegative().describe("\uC0AD\uC81C\uD560 \uC5F4 \uC778\uB371\uC2A4 (0-based)")
13395
13902
  });
13396
- var mergeCellsOpSchema = z10.object({
13397
- type: z10.literal("mergeCells"),
13398
- startRow: z10.number().int().nonnegative().describe("\uBCD1\uD569 \uC2DC\uC791 \uD589 (0-based)"),
13399
- startCol: z10.number().int().nonnegative().describe("\uBCD1\uD569 \uC2DC\uC791 \uC5F4 (0-based)"),
13400
- endRow: z10.number().int().nonnegative().describe("\uBCD1\uD569 \uB05D \uD589 (0-based, \uD3EC\uD568)"),
13401
- endCol: z10.number().int().nonnegative().describe("\uBCD1\uD569 \uB05D \uC5F4 (0-based, \uD3EC\uD568)")
13903
+ var mergeCellsOpSchema = z12.object({
13904
+ type: z12.literal("mergeCells"),
13905
+ startRow: z12.number().int().nonnegative().describe("\uBCD1\uD569 \uC2DC\uC791 \uD589 (0-based)"),
13906
+ startCol: z12.number().int().nonnegative().describe("\uBCD1\uD569 \uC2DC\uC791 \uC5F4 (0-based)"),
13907
+ endRow: z12.number().int().nonnegative().describe("\uBCD1\uD569 \uB05D \uD589 (0-based, \uD3EC\uD568)"),
13908
+ endCol: z12.number().int().nonnegative().describe("\uBCD1\uD569 \uB05D \uC5F4 (0-based, \uD3EC\uD568)")
13402
13909
  });
13403
- var operationSchema = z10.discriminatedUnion("type", [
13910
+ var operationSchema = z12.discriminatedUnion("type", [
13404
13911
  insertRowOpSchema,
13405
13912
  deleteRowOpSchema,
13406
13913
  insertColumnOpSchema,
13407
13914
  deleteColumnOpSchema,
13408
13915
  mergeCellsOpSchema
13409
13916
  ]).describe("\uD45C \uAD6C\uC870 \uC5F0\uC0B0. \uB098\uC911 \uC5F0\uC0B0\uC758 row/col \uC778\uB371\uC2A4\uB294 \uC55E \uC5F0\uC0B0 \uC801\uC6A9 \uD6C4 \uC0C1\uD0DC\uB97C \uAE30\uC900\uC73C\uB85C \uD55C\uB2E4.");
13410
- var proposeTableStructureSchema = z10.object({
13411
- path: z10.string().describe("\uC218\uC815\uD560 .hwpx \uD30C\uC77C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
13412
- anchor: z10.string().min(1).describe(
13917
+ var proposeTableStructureSchema = z12.object({
13918
+ path: z12.string().describe("\uC218\uC815\uD560 .hwpx \uD30C\uC77C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
13919
+ anchor: z12.string().min(1).describe(
13413
13920
  "\uB300\uC0C1 \uD45C\uB97C \uC2DD\uBCC4\uD558\uB294 \uC575\uCEE4 \uD14D\uC2A4\uD2B8. \uD45C \uC548\uC5D0\uB9CC \uC788\uB294 \uB3C5\uD2B9\uD55C \uC140 \uD14D\uC2A4\uD2B8\uB97C \uC9C0\uC815\uD558\uC138\uC694. (\uBD80\uBD84 \uC77C\uCE58, \uACF5\uBC31 \uD2B8\uB9BC) \u2014 read_document\uB85C \uD655\uC778 \uD6C4 \uC0AC\uC6A9 \uAD8C\uC7A5."
13414
13921
  ),
13415
- operations: z10.array(operationSchema).min(1).describe(
13922
+ operations: z12.array(operationSchema).min(1).describe(
13416
13923
  "\uC801\uC6A9\uD560 \uD45C \uAD6C\uC870 \uC5F0\uC0B0 \uBAA9\uB85D (\uC21C\uC11C\uB300\uB85C \uC2E4\uD589). \uAC01 \uC5F0\uC0B0\uC740 \uC774\uC804 \uC5F0\uC0B0\uC774 \uC801\uC6A9\uB41C \uD6C4\uC758 \uD45C \uC0C1\uD0DC \uAE30\uC900\uC73C\uB85C row/col\uC744 \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4."
13417
13924
  ),
13418
- summary: z10.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
13925
+ summary: z12.string().describe("\uBCC0\uACBD \uC694\uC57D (\uD55C\uAD6D\uC5B4 1-2\uBB38\uC7A5)")
13419
13926
  });
13420
13927
  function tokenizeHwpxXml2(xml) {
13421
13928
  const tokens = [];
@@ -13972,7 +14479,7 @@ function getTblDims(tblXml) {
13972
14479
  return parseTblDimensions(tblXml, 0);
13973
14480
  }
13974
14481
  async function applyOpsToHwpx(hwpxBuffer, anchor, operations) {
13975
- const zip = await import_jszip4.default.loadAsync(hwpxBuffer);
14482
+ const zip = await import_jszip6.default.loadAsync(hwpxBuffer);
13976
14483
  const sectionFiles = Object.keys(zip.files).filter((name) => /^Contents\/section\d+\.xml$/.test(name)).sort();
13977
14484
  let targetSectionIdx = -1;
13978
14485
  let targetRange = null;
@@ -14031,7 +14538,7 @@ async function applyOpsToHwpx(hwpxBuffer, anchor, operations) {
14031
14538
  const afterDims = getTblDims(tblBlock);
14032
14539
  const newSectionXml = sectionXml.substring(0, targetRange.start) + tblBlock + sectionXml.substring(targetRange.end);
14033
14540
  sectionXmls[targetSectionIdx] = newSectionXml;
14034
- const out = new import_jszip4.default();
14541
+ const out = new import_jszip6.default();
14035
14542
  const mimetypeEntry = zip.file("mimetype");
14036
14543
  if (mimetypeEntry) {
14037
14544
  out.file("mimetype", await mimetypeEntry.async("uint8array"), { compression: "STORE" });
@@ -14055,7 +14562,7 @@ async function applyOpsToHwpx(hwpxBuffer, anchor, operations) {
14055
14562
  };
14056
14563
  }
14057
14564
  async function verifyOutputDims(newBytes, anchor, expectedRowCnt, expectedColCnt) {
14058
- const zip = await import_jszip4.default.loadAsync(newBytes);
14565
+ const zip = await import_jszip6.default.loadAsync(newBytes);
14059
14566
  const sectionFiles = Object.keys(zip.files).filter((name) => /^Contents\/section\d+\.xml$/.test(name)).sort();
14060
14567
  for (const sf of sectionFiles) {
14061
14568
  const entry = zip.file(sf);
@@ -14090,7 +14597,7 @@ var proposeTableStructureTool = {
14090
14597
  ctx
14091
14598
  }) => {
14092
14599
  const safePath = await resolveSafePath(ctx.cwd, input.path);
14093
- const ext = extname10(safePath).toLowerCase();
14600
+ const ext = extname12(safePath).toLowerCase();
14094
14601
  if (ext === ".hwp") {
14095
14602
  return "\uC624\uB958: propose_table_structure\uB294 .hwpx \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. .hwp(\uAD6C\uD615 OLE \uBC14\uC774\uB108\uB9AC)\uB294 XML \uC9C1\uC811 \uD3B8\uC9D1\uC774 \uBD88\uAC00\uD569\uB2C8\uB2E4. \uD55C\uAE00 \uD504\uB85C\uADF8\uB7A8\uC5D0\uC11C '\uB2E4\uB978 \uC774\uB984\uC73C\uB85C \uC800\uC7A5 \u2192 .hwpx'\uB85C \uC800\uC7A5\uD55C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.";
14096
14603
  }
@@ -14099,7 +14606,7 @@ var proposeTableStructureTool = {
14099
14606
  }
14100
14607
  let originalBuf;
14101
14608
  try {
14102
- originalBuf = await readFile10(safePath);
14609
+ originalBuf = await readFile12(safePath);
14103
14610
  } catch {
14104
14611
  return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input.path}. \uACBD\uB85C\uB97C \uD655\uC778\uD558\uC138\uC694.`;
14105
14612
  }
@@ -14295,11 +14802,11 @@ function searchExcerpts(markdown, query) {
14295
14802
  }
14296
14803
  return result;
14297
14804
  }
14298
- var readDocumentSchema = z11.object({
14299
- path: z11.string().describe("\uC77D\uC744 \uBB38\uC11C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
14300
- pages: z11.string().optional().describe('\uC77D\uC744 \uD398\uC774\uC9C0 \uBC94\uC704 (\uC608: "1-3", "1,3,5") \u2014 \uBBF8\uC9C0\uC815 \uC2DC \uC804\uCCB4'),
14301
- outline: z11.boolean().optional().describe("\uD5E4\uB529(\uC81C\uBAA9) \uAD6C\uC870\uB9CC \uBC18\uD658\uD574 \uBB38\uC11C \uAC1C\uC694 \uD30C\uC545 (\uB300\uD615 \uBB38\uC11C \uD0D0\uC0C9\uC6A9)"),
14302
- search: z11.string().optional().describe("\uD0A4\uC6CC\uB4DC\uAC00 \uD3EC\uD568\uB41C \uBD80\uBD84\uACFC \uC8FC\uBCC0 \uB9E5\uB77D\uB9CC \uBC18\uD658 (\uB300\uD615 \uBB38\uC11C\uC5D0\uC11C \uD544\uC694\uD55C \uBD80\uBD84\uB9CC \uC77D\uAE30)")
14805
+ var readDocumentSchema = z13.object({
14806
+ path: z13.string().describe("\uC77D\uC744 \uBB38\uC11C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
14807
+ pages: z13.string().optional().describe('\uC77D\uC744 \uD398\uC774\uC9C0 \uBC94\uC704 (\uC608: "1-3", "1,3,5") \u2014 \uBBF8\uC9C0\uC815 \uC2DC \uC804\uCCB4'),
14808
+ outline: z13.boolean().optional().describe("\uD5E4\uB529(\uC81C\uBAA9) \uAD6C\uC870\uB9CC \uBC18\uD658\uD574 \uBB38\uC11C \uAC1C\uC694 \uD30C\uC545 (\uB300\uD615 \uBB38\uC11C \uD0D0\uC0C9\uC6A9)"),
14809
+ search: z13.string().optional().describe("\uD0A4\uC6CC\uB4DC\uAC00 \uD3EC\uD568\uB41C \uBD80\uBD84\uACFC \uC8FC\uBCC0 \uB9E5\uB77D\uB9CC \uBC18\uD658 (\uB300\uD615 \uBB38\uC11C\uC5D0\uC11C \uD544\uC694\uD55C \uBD80\uBD84\uB9CC \uC77D\uAE30)")
14303
14810
  });
14304
14811
  function applyReadMode(body, outline, search) {
14305
14812
  if (outline === true) {
@@ -14328,11 +14835,11 @@ var readDocumentTool = {
14328
14835
  const msg = e instanceof Error ? e.message : String(e);
14329
14836
  return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${msg}`;
14330
14837
  }
14331
- const ext = extname11(safePath).toLowerCase();
14838
+ const ext = extname13(safePath).toLowerCase();
14332
14839
  if (PLAIN_TEXT_EXTS.has(ext)) {
14333
14840
  let raw;
14334
14841
  try {
14335
- raw = await readFile11(safePath, "utf-8");
14842
+ raw = await readFile13(safePath, "utf-8");
14336
14843
  } catch (e) {
14337
14844
  const msg = e instanceof Error ? e.message : String(e);
14338
14845
  return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${msg}`;
@@ -14432,8 +14939,8 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
14432
14939
  ".csv",
14433
14940
  ".log"
14434
14941
  ]);
14435
- var readFileSchema = z12.object({
14436
- path: z12.string().describe("\uC77D\uC744 \uD30C\uC77C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)")
14942
+ var readFileSchema = z14.object({
14943
+ path: z14.string().describe("\uC77D\uC744 \uD30C\uC77C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)")
14437
14944
  });
14438
14945
  var readFileTool = {
14439
14946
  name: "read_file",
@@ -14457,8 +14964,8 @@ var readFileTool = {
14457
14964
  if (info.size > MAX_SIZE) {
14458
14965
  return `\uC624\uB958: \uD30C\uC77C\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${Math.round(info.size / 1024)}KB). \uCD5C\uB300 256KB\uAE4C\uC9C0 \uC77D\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`;
14459
14966
  }
14460
- const { extname: extname14 } = await import("path");
14461
- const ext = extname14(safePath).toLowerCase();
14967
+ const { extname: extname17 } = await import("path");
14968
+ const ext = extname17(safePath).toLowerCase();
14462
14969
  if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
14463
14970
  return `\uC624\uB958: '${ext}' \uD615\uC2DD\uC740 \uD14D\uC2A4\uD2B8 \uD30C\uC77C\uB85C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uC11C \uD30C\uC77C\uC774\uB77C\uBA74 read_document \uD234\uC744 \uC0AC\uC6A9\uD558\uC138\uC694.`;
14464
14971
  }
@@ -14472,10 +14979,64 @@ var readFileTool = {
14472
14979
  return content;
14473
14980
  }
14474
14981
  };
14982
+ var PLAIN_TEXT_EXTS2 = /* @__PURE__ */ new Set([".md", ".markdown", ".txt", ".text"]);
14983
+ var scanPiiSchema = z15.object({
14984
+ path: z15.string().describe("\uAC1C\uC778\uC815\uBCF4\uB97C \uC810\uAC80\uD560 \uBB38\uC11C \uACBD\uB85C")
14985
+ });
14986
+ var scanPiiTool = {
14987
+ name: "scan_pii",
14988
+ description: "\uBB38\uC11C\uC5D0\uC11C \uD55C\uAD6D \uAC1C\uC778\uC815\uBCF4(\uC8FC\uBBFC\uB4F1\uB85D\uBC88\uD638\xB7\uC2E0\uC6A9\uCE74\uB4DC\uBC88\uD638\xB7\uC804\uD654\uBC88\uD638\xB7\uC774\uBA54\uC77C)\uB97C \uD0D0\uC9C0\uD569\uB2C8\uB2E4. \uC6D0\uBB38 \uAC12\uC740 \uBC18\uD658\uD558\uC9C0 \uC54A\uACE0 \uB9C8\uC2A4\uD0B9\uB41C \uC608\uC2DC\uB9CC \uD45C\uC2DC\uD569\uB2C8\uB2E4. \uC678\uBD80 \uACF5\uC720 \uC804 \uAC80\uD1A0\uC5D0 \uD65C\uC6A9\uD558\uC138\uC694.",
14989
+ inputSchema: scanPiiSchema,
14990
+ requiresApproval: false,
14991
+ execute: async ({
14992
+ input,
14993
+ ctx
14994
+ }) => {
14995
+ let safePath;
14996
+ try {
14997
+ safePath = await resolveSafePath(ctx.cwd, input.path);
14998
+ } catch (e) {
14999
+ const msg = e instanceof Error ? e.message : String(e);
15000
+ return `\uC624\uB958: \uACBD\uB85C\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${msg}`;
15001
+ }
15002
+ const ext = extname14(safePath).toLowerCase();
15003
+ let text3;
15004
+ if (PLAIN_TEXT_EXTS2.has(ext)) {
15005
+ try {
15006
+ text3 = await readFile14(safePath, "utf-8");
15007
+ } catch (e) {
15008
+ const msg = e instanceof Error ? e.message : String(e);
15009
+ return `\uC624\uB958: \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${msg}`;
15010
+ }
15011
+ } else {
15012
+ let parseResult;
15013
+ try {
15014
+ parseResult = await parse6(safePath);
15015
+ } catch (e) {
15016
+ const msg = e instanceof Error ? e.message : String(e);
15017
+ return `\uC624\uB958: \uBB38\uC11C\uB97C \uD30C\uC2F1\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${msg}`;
15018
+ }
15019
+ if (!parseResult.success) {
15020
+ return `\uC624\uB958: \uBB38\uC11C\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${parseResult.error ?? "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}`;
15021
+ }
15022
+ text3 = parseResult.markdown;
15023
+ }
15024
+ const findings = detectPii(text3);
15025
+ if (findings.length === 0) {
15026
+ return `\uAC1C\uC778\uC815\uBCF4\uAC00 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4: ${input.path}`;
15027
+ }
15028
+ const lines = [`\uBC1C\uACAC\uB41C \uAC1C\uC778\uC815\uBCF4 (${input.path}):`];
15029
+ for (const f of findings) {
15030
+ lines.push(`- ${f.type}: ${f.count}\uAC74 (\uC608: ${f.masked.join(", ")})`);
15031
+ }
15032
+ lines.push("\u203B \uB9C8\uC2A4\uD0B9\uB41C \uC608\uC2DC\uC774\uBA70 \uC6D0\uBB38 \uAC12\uC740 \uD45C\uC2DC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC678\uBD80 \uACF5\uC720 \uC804 \uD655\uC778\uD558\uC138\uC694.");
15033
+ return lines.join("\n");
15034
+ }
15035
+ };
14475
15036
  var MAX_PREVIEW_CHARS = 1e4;
14476
- var writeNewDocumentSchema = z13.object({
14477
- path: z13.string().describe("\uC0DD\uC131\uD560 \uBB38\uC11C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
14478
- markdown: z13.string().describe("\uC0C8 \uBB38\uC11C \uB0B4\uC6A9 (\uB9C8\uD06C\uB2E4\uC6B4 \uD615\uC2DD)")
15037
+ var writeNewDocumentSchema = z16.object({
15038
+ path: z16.string().describe("\uC0DD\uC131\uD560 \uBB38\uC11C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
15039
+ markdown: z16.string().describe("\uC0C8 \uBB38\uC11C \uB0B4\uC6A9 (\uB9C8\uD06C\uB2E4\uC6B4 \uD615\uC2DD)")
14479
15040
  });
14480
15041
  var writeNewDocumentTool = {
14481
15042
  name: "write_new_document",
@@ -14487,7 +15048,7 @@ var writeNewDocumentTool = {
14487
15048
  ctx
14488
15049
  }) => {
14489
15050
  const safePath = await resolveSafePath(ctx.cwd, input.path);
14490
- const ext = extname12(safePath).toLowerCase();
15051
+ const ext = extname15(safePath).toLowerCase();
14491
15052
  try {
14492
15053
  await stat6(safePath);
14493
15054
  return `\uC624\uB958: \uD30C\uC77C\uC774 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4: ${input.path}. \uAE30\uC874 \uD30C\uC77C\uC744 \uC218\uC815\uD558\uB824\uBA74 propose_edit\uC744 \uC0AC\uC6A9\uD558\uC138\uC694.`;
@@ -14537,12 +15098,12 @@ ${preview}`,
14537
15098
  };
14538
15099
  }
14539
15100
  };
14540
- var writeNewSpreadsheetSchema = z14.object({
14541
- path: z14.string().describe("\uC0DD\uC131\uD560 XLSX \uD30C\uC77C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
14542
- sheets: z14.array(
14543
- z14.object({
14544
- name: z14.string().describe("\uC2DC\uD2B8 \uC774\uB984"),
14545
- rows: z14.array(z14.array(z14.string())).describe("\uD589 \uB370\uC774\uD130 \uBC30\uC5F4 (\uAC01 \uD589\uC740 \uC140 \uAC12 \uBC30\uC5F4)")
15101
+ var writeNewSpreadsheetSchema = z17.object({
15102
+ path: z17.string().describe("\uC0DD\uC131\uD560 XLSX \uD30C\uC77C \uACBD\uB85C (cwd \uAE30\uC900 \uC0C1\uB300 \uACBD\uB85C \uB610\uB294 \uC808\uB300 \uACBD\uB85C)"),
15103
+ sheets: z17.array(
15104
+ z17.object({
15105
+ name: z17.string().describe("\uC2DC\uD2B8 \uC774\uB984"),
15106
+ rows: z17.array(z17.array(z17.string())).describe("\uD589 \uB370\uC774\uD130 \uBC30\uC5F4 (\uAC01 \uD589\uC740 \uC140 \uAC12 \uBC30\uC5F4)")
14546
15107
  })
14547
15108
  ).min(1).describe("\uC0DD\uC131\uD560 \uC2DC\uD2B8 \uBAA9\uB85D")
14548
15109
  });
@@ -14556,7 +15117,7 @@ var writeNewSpreadsheetTool = {
14556
15117
  ctx
14557
15118
  }) => {
14558
15119
  const safePath = await resolveSafePath(ctx.cwd, input.path);
14559
- const ext = extname13(safePath).toLowerCase();
15120
+ const ext = extname16(safePath).toLowerCase();
14560
15121
  if (ext !== ".xlsx") {
14561
15122
  return `\uC624\uB958: write_new_spreadsheet\uC740 .xlsx \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4. \uD604\uC7AC \uD655\uC7A5\uC790: ${ext}.`;
14562
15123
  }
@@ -14616,10 +15177,13 @@ function createDocTools(_ctx) {
14616
15177
  listFilesTool,
14617
15178
  listBackupsTool,
14618
15179
  readFileTool,
15180
+ scanPiiTool,
15181
+ findInDocumentTool,
14619
15182
  proposeEditTool,
14620
15183
  proposeFormFillTool,
14621
15184
  proposeCellEditTool,
14622
15185
  proposeFindReplaceTool,
15186
+ proposeRedactPiiTool,
14623
15187
  proposeSheetEditTool,
14624
15188
  proposeTableStructureTool,
14625
15189
  listFormObjectsTool,
@@ -15443,10 +16007,10 @@ async function runOnboarding() {
15443
16007
 
15444
16008
  // src/update.ts
15445
16009
  import { spawn } from "child_process";
15446
- import { mkdir as mkdir4, readFile as readFile12, writeFile as writeFile3 } from "fs/promises";
16010
+ import { mkdir as mkdir4, readFile as readFile15, writeFile as writeFile3 } from "fs/promises";
15447
16011
  import { dirname as dirname3 } from "path";
15448
16012
  function compareSemver(a, b) {
15449
- const parse6 = (v) => {
16013
+ const parse7 = (v) => {
15450
16014
  const clean = v.replace(/^v/, "").split("-")[0] ?? "";
15451
16015
  const parts = clean.split(".").map((p) => {
15452
16016
  const n = parseInt(p, 10);
@@ -15454,8 +16018,8 @@ function compareSemver(a, b) {
15454
16018
  });
15455
16019
  return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
15456
16020
  };
15457
- const [aMaj, aMin, aPat] = parse6(a);
15458
- const [bMaj, bMin, bPat] = parse6(b);
16021
+ const [aMaj, aMin, aPat] = parse7(a);
16022
+ const [bMaj, bMin, bPat] = parse7(b);
15459
16023
  if (aMaj !== bMaj) return aMaj < bMaj ? -1 : 1;
15460
16024
  if (aMin !== bMin) return aMin < bMin ? -1 : 1;
15461
16025
  if (aPat !== bPat) return aPat < bPat ? -1 : 1;
@@ -15464,7 +16028,7 @@ function compareSemver(a, b) {
15464
16028
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
15465
16029
  async function readCache(cachePath) {
15466
16030
  try {
15467
- const raw = await readFile12(cachePath, "utf-8");
16031
+ const raw = await readFile15(cachePath, "utf-8");
15468
16032
  const parsed = JSON.parse(raw);
15469
16033
  if (parsed !== null && typeof parsed === "object" && "checkedAt" in parsed && "latest" in parsed && typeof parsed.checkedAt === "string" && typeof parsed.latest === "string") {
15470
16034
  return parsed;