@simplysm/sd-claude 13.0.71 → 13.0.72

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.
@@ -0,0 +1,7 @@
1
+ # Simplysm Library Issue Reporting
2
+
3
+ Source code for `@simplysm/*` packages can be found in `node_modules/@simplysm/`. If debugging reveals the root cause is in the simplysm library itself, generate a GitHub issue-formatted text (title, reproduction steps, expected behavior, actual behavior) and display it to the user.
4
+
5
+ **Report facts only — do not suggest fixes or include code location hints. Do not auto-submit the issue — only display the text.**
6
+
7
+ The issue body must NEVER include internal analysis of library code (class names, variable names, style properties, inheritance chains, etc.). Only describe user-observable symptoms.
@@ -21,9 +21,9 @@ If a referenced file or document cannot be found, **stop immediately and ask the
21
21
 
22
22
  ### Questions vs. Code Requests — CRITICAL
23
23
 
24
- - **If the user asks a question** (e.g., "이건 이래?", "이거 뭐야?", "어떻게 동작해?") → **answer with text only**. Do NOT edit, write, or create any files.
24
+ - **If the user asks a question** (e.g., "Why is this like this?", "What is this?", "How does this work?") → **answer with text only**. Do NOT edit, write, or create any files.
25
25
  - **If the user discusses, explains, or shares opinions** → **respond with text only**. Do NOT touch any files.
26
- - **Only edit/write/create files when the user explicitly requests code changes** (e.g., "수정해줘", "만들어줘", "변경해줘", "추가해줘", "고쳐줘").
26
+ - **Only edit/write/create files when the user explicitly requests code changes** (e.g., "Fix this", "Create this", "Change this", "Add this").
27
27
  - Reading files to answer a question is fine. **Modifying files to answer a question is prohibited.**
28
28
 
29
29
  ### General Rules
@@ -19,6 +19,7 @@ Determine the major version by the `version` field in `package.json`.
19
19
  | Debugging, problem-solving, or planning approach | `.claude/refs/sd-workflow.md` |
20
20
  | Using `@simplysm/service-*` packages | `.claude/refs/sd-service.md` |
21
21
  | Migrating/porting code from another codebase | `.claude/refs/sd-migration.md` |
22
+ | Debugging in a project that uses `@simplysm/*` as an external dependency (not the simplysm monorepo itself) | `.claude/refs/sd-library-issue.md` |
22
23
 
23
24
  ## v12 only (< 13)
24
25
 
@@ -241,7 +241,7 @@ async function main() {
241
241
  const cwd = input.cwd ?? process.cwd();
242
242
  const folderName = path.basename(cwd);
243
243
 
244
- // 출력
244
+ // Output
245
245
  const dailyStr = dailyResetTime ? `${dailyPercent}%(${dailyResetTime})` : `${dailyPercent}%`;
246
246
  const weekStr = weekResetDay ? `${weekPercent}%(${weekResetDay})` : `${weekPercent}%`;
247
247
  const parts = [folderName, modelName, `${contextPercent}%`, dailyStr, weekStr];
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """DOCX 파일에서 텍스트와 이미지를 문단 흐름 순서로 추출한다."""
2
+ """Extract text and images from DOCX files in paragraph flow order."""
3
3
 
4
4
  import sys
5
5
  import io
@@ -17,7 +17,7 @@ def ensure_packages():
17
17
  try:
18
18
  __import__(import_name)
19
19
  except ImportError:
20
- print(f"패키지 설치 중: {pip_name}...", file=sys.stderr)
20
+ print(f"Installing package: {pip_name}...", file=sys.stderr)
21
21
  subprocess.check_call([sys.executable, "-m", "pip", "install", pip_name],
22
22
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
23
23
 
@@ -71,7 +71,7 @@ def extract(file_path):
71
71
  elif not has_image and not text:
72
72
  continue
73
73
 
74
- # 추출
74
+ # Table extraction
75
75
  for t_idx, table in enumerate(doc.tables):
76
76
  print(f"\n### Table {t_idx + 1}\n")
77
77
  for row in table.rows:
@@ -79,9 +79,9 @@ def extract(file_path):
79
79
  print("| " + " | ".join(cells) + " |")
80
80
 
81
81
  if img_idx > 0:
82
- print(f"\n---\n이미지 {img_idx} 저장: {out_dir}")
82
+ print(f"\n---\n{img_idx} image(s) saved: {out_dir}")
83
83
  else:
84
- print("\n---\n이미지 없음")
84
+ print("\n---\nNo images")
85
85
 
86
86
 
87
87
  if __name__ == "__main__":
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """PDF 파일에서 텍스트, 표, 이미지를 페이지별로 추출한다."""
2
+ """Extract text, tables, and images from PDF files page by page."""
3
3
 
4
4
  import sys
5
5
  import io
@@ -16,7 +16,7 @@ def ensure_packages():
16
16
  try:
17
17
  __import__(import_name)
18
18
  except ImportError:
19
- print(f"패키지 설치 중: {pip_name}...", file=sys.stderr)
19
+ print(f"Installing package: {pip_name}...", file=sys.stderr)
20
20
  subprocess.check_call([sys.executable, "-m", "pip", "install", pip_name],
21
21
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
22
22
 
@@ -32,7 +32,7 @@ def extract(file_path):
32
32
 
33
33
  print(f"# {Path(file_path).name}\n")
34
34
 
35
- # 텍스트 + 추출 (pdfplumber)
35
+ # Text + table extraction (pdfplumber)
36
36
  with pdfplumber.open(file_path) as pdf:
37
37
  for page_num, page in enumerate(pdf.pages, 1):
38
38
  print(f"## Page {page_num}\n")
@@ -52,7 +52,7 @@ def extract(file_path):
52
52
  print("| " + " | ".join(cells) + " |")
53
53
  print()
54
54
 
55
- # 이미지 추출 (pypdf)
55
+ # Image extraction (pypdf)
56
56
  reader = PdfReader(file_path)
57
57
  for page_num, page in enumerate(reader.pages, 1):
58
58
  if "/XObject" not in (page.get("/Resources") or {}):
@@ -79,19 +79,19 @@ def extract(file_path):
79
79
  img_path.write_bytes(obj._data if hasattr(obj, "_data") else b"")
80
80
  print(f"[IMG] (page={page_num}) {img_path}")
81
81
 
82
- # OCR 안내
82
+ # OCR notice
83
83
  if total_text_len == 0:
84
- print("\n⚠ 텍스트가 추출되지 않았습니다 (스캔 PDF일 있음).")
85
- print("OCR 필요합니다:")
86
- print(" 1. Tesseract OCR 설치: https://github.com/tesseract-ocr/tesseract")
84
+ print("\n⚠ No text was extracted (may be a scanned PDF).")
85
+ print("OCR is required:")
86
+ print(" 1. Install Tesseract OCR: https://github.com/tesseract-ocr/tesseract")
87
87
  print(" 2. pip install pytesseract pdf2image")
88
- print(" 3. pytesseract.image_to_string() 으로 추출")
88
+ print(" 3. Extract with pytesseract.image_to_string()")
89
89
 
90
90
  print()
91
91
  if img_idx > 0:
92
- print(f"---\n이미지 {img_idx} 저장: {out_dir}")
92
+ print(f"---\n{img_idx} image(s) saved: {out_dir}")
93
93
  else:
94
- print("---\n이미지 없음")
94
+ print("---\nNo images")
95
95
 
96
96
 
97
97
  if __name__ == "__main__":
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """PPTX 파일에서 텍스트와 이미지를 슬라이드별 좌표와 함께 추출한다."""
2
+ """Extract text and images from PPTX files with per-slide coordinates."""
3
3
 
4
4
  import sys
5
5
  import io
@@ -16,13 +16,13 @@ def ensure_packages():
16
16
  try:
17
17
  __import__(import_name)
18
18
  except ImportError:
19
- print(f"패키지 설치 중: {pip_name}...", file=sys.stderr)
19
+ print(f"Installing package: {pip_name}...", file=sys.stderr)
20
20
  subprocess.check_call([sys.executable, "-m", "pip", "install", pip_name],
21
21
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
22
22
 
23
23
 
24
24
  def emu_to_inches(emu):
25
- """EMU 인치로 변환 (소수점 1자리)."""
25
+ """Convert EMU to inches (1 decimal place)."""
26
26
  if emu is None:
27
27
  return "?"
28
28
  return f"{emu / 914400:.1f}"
@@ -64,9 +64,9 @@ def extract(file_path):
64
64
  print()
65
65
 
66
66
  if img_idx > 0:
67
- print(f"---\n이미지 {img_idx} 저장: {out_dir}")
67
+ print(f"---\n{img_idx} image(s) saved: {out_dir}")
68
68
  else:
69
- print("---\n이미지 없음")
69
+ print("---\nNo images")
70
70
 
71
71
 
72
72
  if __name__ == "__main__":
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """XLSX 파일에서 데이터와 이미지를 위치와 함께 추출한다."""
2
+ """Extract data and images from XLSX files with cell positions."""
3
3
 
4
4
  import sys
5
5
  import io
@@ -17,7 +17,7 @@ def ensure_packages():
17
17
  try:
18
18
  __import__(import_name)
19
19
  except ImportError:
20
- print(f"패키지 설치 중: {pip_name}...", file=sys.stderr)
20
+ print(f"Installing package: {pip_name}...", file=sys.stderr)
21
21
  subprocess.check_call([sys.executable, "-m", "pip", "install", pip_name],
22
22
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
23
23
 
@@ -37,10 +37,10 @@ def extract(file_path):
37
37
  ws = wb[sheet_name]
38
38
  print(f"## Sheet: {sheet_name}\n")
39
39
 
40
- # 데이터 추출
40
+ # Data extraction
41
41
  rows = list(ws.iter_rows(values_only=False))
42
42
  if not rows:
43
- print("( 시트)\n")
43
+ print("(empty sheet)\n")
44
44
  continue
45
45
 
46
46
  for row in rows:
@@ -53,7 +53,7 @@ def extract(file_path):
53
53
  cells.append(str(val).strip())
54
54
  print(f"[{row[0].coordinate.split('1')[0]}{row[0].row}] " + " | ".join(cells))
55
55
 
56
- # 이미지 추출
56
+ # Image extraction
57
57
  if ws._images:
58
58
  for img in ws._images:
59
59
  img_idx += 1
@@ -70,9 +70,9 @@ def extract(file_path):
70
70
  print()
71
71
 
72
72
  if img_idx > 0:
73
- print(f"---\n이미지 {img_idx} 저장: {out_dir}")
73
+ print(f"---\n{img_idx} image(s) saved: {out_dir}")
74
74
  else:
75
- print("---\n이미지 없음")
75
+ print("---\nNo images")
76
76
 
77
77
 
78
78
  if __name__ == "__main__":
@@ -12,13 +12,13 @@ import base64
12
12
  from email.policy import default as default_policy
13
13
  from pathlib import Path
14
14
 
15
- # stdout UTF-8 강제 (Windows 호환)
15
+ # Force stdout UTF-8 (Windows compatibility)
16
16
  sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
17
17
  sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
18
18
 
19
19
 
20
20
  def ensure_packages():
21
- """필요한 패키지 자동 설치."""
21
+ """Auto-install required packages."""
22
22
  packages = {"extract-msg": "extract_msg"}
23
23
  missing = []
24
24
  for pip_name, import_name in packages.items():
@@ -27,7 +27,7 @@ def ensure_packages():
27
27
  except ImportError:
28
28
  missing.append(pip_name)
29
29
  if missing:
30
- print(f"패키지 설치 중: {', '.join(missing)}...", file=sys.stderr)
30
+ print(f"Installing packages: {', '.join(missing)}...", file=sys.stderr)
31
31
  subprocess.check_call(
32
32
  [sys.executable, "-m", "pip", "install", "-q", *missing],
33
33
  stdout=subprocess.DEVNULL,
@@ -326,45 +326,45 @@ def build_report(filepath):
326
326
  saved_attachments = save_files(attachments, out_dir)
327
327
 
328
328
  out = []
329
- out.append("# 이메일 분석서\n")
330
- out.append(f"**원본 파일**: `{os.path.basename(filepath)}`\n")
331
-
332
- # ── 메일 정보
333
- out.append("## 메일 정보\n")
334
- out.append("| 항목 | 내용 |")
335
- out.append("|------|------|")
336
- out.append(f"| **제목** | {headers['subject']} |")
337
- out.append(f"| **보낸 사람** | {headers['from']} |")
338
- out.append(f"| **받는 사람** | {headers['to']} |")
329
+ out.append("# Email Analysis Report\n")
330
+ out.append(f"**Source file**: `{os.path.basename(filepath)}`\n")
331
+
332
+ # ── Email info
333
+ out.append("## Email Info\n")
334
+ out.append("| Field | Value |")
335
+ out.append("|-------|-------|")
336
+ out.append(f"| **Subject** | {headers['subject']} |")
337
+ out.append(f"| **From** | {headers['from']} |")
338
+ out.append(f"| **To** | {headers['to']} |")
339
339
  if headers["cc"]:
340
- out.append(f"| **참조** | {headers['cc']} |")
341
- out.append(f"| **날짜** | {headers['date']} |")
342
- out.append(f"| **첨부파일** | {len(saved_attachments)} |")
340
+ out.append(f"| **CC** | {headers['cc']} |")
341
+ out.append(f"| **Date** | {headers['date']} |")
342
+ out.append(f"| **Attachments** | {len(saved_attachments)} |")
343
343
  if all_inline:
344
- out.append(f"| **본문 이미지** | {len(all_inline)} |")
344
+ out.append(f"| **Inline images** | {len(all_inline)} |")
345
345
  out.append("")
346
346
 
347
- # ── 본문
348
- out.append("## 본문 내용\n")
347
+ # ── Body
348
+ out.append("## Body\n")
349
349
  body = body_plain
350
350
  if not body and body_html:
351
351
  body = strip_html(body_html)
352
- out.append(body.strip() if body else "_(본문 없음)_")
352
+ out.append(body.strip() if body else "_(No body)_")
353
353
  out.append("")
354
354
 
355
- # ── 본문 삽입 이미지
355
+ # ── Inline images
356
356
  if all_inline:
357
- out.append("## 본문 삽입 이미지\n")
358
- out.append("| # | 파일명 | 크기 | 저장 경로 |")
357
+ out.append("## Inline Images\n")
358
+ out.append("| # | Filename | Size | Saved path |")
359
359
  out.append("|---|--------|------|-----------|")
360
360
  for i, img in enumerate(all_inline, 1):
361
361
  out.append(f"| {i} | {img['filename']} | {fmt_size(img['size'])} | `{img['saved_path']}` |")
362
362
  out.append("")
363
363
 
364
- # ── 첨부파일
364
+ # ── Attachments
365
365
  if saved_attachments:
366
- out.append("## 첨부파일\n")
367
- out.append("| # | 파일명 | 크기 | 저장 경로 |")
366
+ out.append("## Attachments\n")
367
+ out.append("| # | Filename | Size | Saved path |")
368
368
  out.append("|---|--------|------|-----------|")
369
369
  for i, a in enumerate(saved_attachments, 1):
370
370
  out.append(f"| {i} | {a['filename']} | {fmt_size(a['size'])} | `{a['saved_path']}` |")
@@ -382,12 +382,12 @@ if __name__ == "__main__":
382
382
 
383
383
  path = sys.argv[1]
384
384
  if not os.path.isfile(path):
385
- print(f"파일을 찾을 없습니다: {path}", file=sys.stderr)
385
+ print(f"File not found: {path}", file=sys.stderr)
386
386
  sys.exit(1)
387
387
 
388
388
  ext = Path(path).suffix.lower()
389
389
  if ext not in (".eml", ".msg"):
390
- print(f"지원하지 않는 형식: {ext} (.eml 또는 .msg 지원)", file=sys.stderr)
390
+ print(f"Unsupported format: {ext} (only .eml and .msg are supported)", file=sys.stderr)
391
391
  sys.exit(1)
392
392
 
393
393
  print(build_report(path))
@@ -16,10 +16,11 @@ model: haiku
16
16
 
17
17
  2. **If `merge` fails or produces conflicts → STOP IMMEDIATELY and show this message:**
18
18
  ```
19
- ⚠️ merge 문제가 발생했습니다.
20
- 직접 수동으로 merge 진행해 주세요.
21
- (에러 내용: <에러/충돌 정보 그대로 출력>)
19
+ ⚠️ A problem occurred during merge.
20
+ Please proceed with the merge manually.
21
+ (Error details: <print error/conflict info as-is>)
22
22
  ```
23
+ - Show this message in the system's configured language.
23
24
  - Do NOT attempt to resolve conflicts yourself.
24
25
  - Do NOT run `git merge --abort` without asking.
25
26
  - Do NOT proceed to `clean` after a failed merge.
@@ -28,10 +29,11 @@ model: haiku
28
29
 
29
30
  3. **If `rebase` fails or produces conflicts → STOP IMMEDIATELY and show this message:**
30
31
  ```
31
- ⚠️ rebase 문제가 발생했습니다.
32
- 직접 수동으로 rebase 진행해 주세요.
33
- (에러 내용: <에러/충돌 정보 그대로 출력>)
32
+ ⚠️ A problem occurred during rebase.
33
+ Please proceed with the rebase manually.
34
+ (Error details: <print error/conflict info as-is>)
34
35
  ```
36
+ - Show this message in the system's configured language.
35
37
  - Same rules as merge. Do NOT auto-resolve. Just show the message and STOP.
36
38
 
37
39
  4. **NEVER run destructive git commands during worktree workflows:**
@@ -102,9 +104,9 @@ node .claude/skills/sd-worktree/sd-worktree.mjs merge [name]
102
104
 
103
105
  **MERGE SAFETY PROTOCOL:**
104
106
  1. Before merge: check BOTH main and worktree for uncommitted changes
105
- 2. If the script exits with non-zero → show "직접 수동으로 merge 주세요" message and STOP.
107
+ 2. If the script exits with non-zero → show "Please proceed with the merge manually." message (in system language) and STOP.
106
108
  3. After merge: run `git status` in main to confirm no conflicts
107
- 4. If conflicts or errors → show "직접 수동으로 merge 주세요" message and STOP.
109
+ 4. If conflicts or errors → show "Please proceed with the merge manually." message (in system language) and STOP.
108
110
  5. Only proceed to `clean` after confirming merge was fully successful
109
111
 
110
112
  ### clean — Remove worktree and delete branch
@@ -13,7 +13,7 @@ function getOutput(command) {
13
13
  return execSync(command, { encoding: "utf-8" }).trim();
14
14
  }
15
15
 
16
- // 메인 working tree 경로 (worktree 안에서 실행해도 정확)
16
+ // Main working tree path (accurate even when run inside a worktree)
17
17
  const mainWorktree = getOutput("git worktree list --porcelain")
18
18
  .split("\n")[0]
19
19
  .replace("worktree ", "");
@@ -71,7 +71,7 @@ switch (cmd) {
71
71
  console.error("Usage: sd-worktree.mjs merge [name] (or run inside .worktrees/<name>)");
72
72
  process.exit(1);
73
73
  }
74
- // uncommitted 변경 확인
74
+ // Check for uncommitted changes
75
75
  const worktreePath_m = resolve(mainWorktree, ".worktrees", name);
76
76
  if (existsSync(worktreePath_m)) {
77
77
  const status = getOutput(`git -C "${worktreePath_m}" status --porcelain`);
@@ -99,7 +99,7 @@ switch (cmd) {
99
99
  console.error(`Error: worktree '${name}' does not exist.`);
100
100
  process.exit(1);
101
101
  }
102
- // uncommitted 변경 확인
102
+ // Check for uncommitted changes
103
103
  const statusR = getOutput(`git -C "${worktreePath_r}" status --porcelain`);
104
104
  if (statusR) {
105
105
  console.error(`Error: worktree '${name}' has uncommitted changes:\n${statusR}`);
@@ -119,7 +119,7 @@ switch (cmd) {
119
119
  console.error("Usage: sd-worktree.mjs clean [name] (or run inside .worktrees/<name>)");
120
120
  process.exit(1);
121
121
  }
122
- // worktree 안에서 실행 차단
122
+ // Block execution from inside the worktree
123
123
  const worktreePath = resolve(mainWorktree, ".worktrees", name);
124
124
  const cwd = process.cwd();
125
125
  if (cwd === worktreePath || cwd.startsWith(worktreePath + "/")) {
@@ -134,7 +134,7 @@ switch (cmd) {
134
134
  try {
135
135
  run(`git worktree remove --force "${worktreePath}"`, { cwd: mainWorktree });
136
136
  } catch {
137
- // node_modules 등으로 git worktree remove 실패 수동 정리
137
+ // Manual cleanup when git worktree remove fails (e.g., due to node_modules)
138
138
  console.log("git worktree remove failed, cleaning up manually...");
139
139
  rmSync(worktreePath, { recursive: true, force: true });
140
140
  run("git worktree prune", { cwd: mainWorktree });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/sd-claude",
3
- "version": "13.0.71",
3
+ "version": "13.0.72",
4
4
  "description": "Simplysm Claude Code CLI — asset installer",
5
5
  "author": "simplysm",
6
6
  "license": "Apache-2.0",