@simplysm/sd-claude 13.0.67 → 13.0.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +5 -19
  2. package/claude/refs/sd-code-conventions.md +6 -0
  3. package/claude/refs/sd-migration.md +7 -0
  4. package/claude/rules/sd-claude-rules.md +2 -20
  5. package/claude/rules/sd-refs-linker.md +1 -0
  6. package/claude/skills/sd-api-name-review/SKILL.md +2 -1
  7. package/claude/skills/sd-brainstorm/SKILL.md +2 -1
  8. package/claude/skills/sd-check/SKILL.md +2 -1
  9. package/claude/skills/sd-commit/SKILL.md +2 -1
  10. package/claude/skills/sd-debug/SKILL.md +2 -1
  11. package/claude/skills/sd-discuss/SKILL.md +2 -1
  12. package/claude/skills/sd-email-analyze/SKILL.md +52 -0
  13. package/claude/skills/sd-email-analyze/email-analyzer.py +393 -0
  14. package/claude/skills/sd-explore/SKILL.md +2 -1
  15. package/claude/skills/sd-plan/SKILL.md +2 -1
  16. package/claude/skills/sd-plan-dev/SKILL.md +2 -1
  17. package/claude/skills/sd-readme/SKILL.md +2 -1
  18. package/claude/skills/sd-review/SKILL.md +2 -1
  19. package/claude/skills/sd-skill/SKILL.md +2 -1
  20. package/claude/skills/sd-tdd/SKILL.md +2 -1
  21. package/claude/skills/sd-use/SKILL.md +28 -23
  22. package/claude/skills/sd-worktree/SKILL.md +2 -1
  23. package/dist/commands/install.d.ts.map +1 -1
  24. package/dist/commands/install.js +0 -31
  25. package/dist/commands/install.js.map +1 -1
  26. package/dist/index.d.ts +0 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +0 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/sd-claude.js +8 -14
  31. package/dist/sd-claude.js.map +1 -1
  32. package/package.json +2 -2
  33. package/src/commands/install.ts +0 -40
  34. package/src/index.ts +0 -1
  35. package/src/sd-claude.ts +14 -23
  36. package/claude/skills/sd-eml-analyze/SKILL.md +0 -48
  37. package/claude/skills/sd-eml-analyze/eml-analyzer.py +0 -335
  38. package/dist/commands/npx.d.ts +0 -2
  39. package/dist/commands/npx.d.ts.map +0 -1
  40. package/dist/commands/npx.js +0 -16
  41. package/dist/commands/npx.js.map +0 -6
  42. package/src/commands/npx.ts +0 -19
@@ -1,335 +0,0 @@
1
- #!/usr/bin/env python3
2
- """EML Email Analyzer - Parses EML files and attachments into structured markdown."""
3
-
4
- import sys
5
- import os
6
- import io
7
- import subprocess
8
- import email
9
- import html
10
- import re
11
- import tempfile
12
- from email.policy import default as default_policy
13
- from email.header import decode_header
14
- from pathlib import Path
15
-
16
- # stdout UTF-8 강제 (Windows 호환)
17
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
18
- sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
19
-
20
-
21
- def ensure_packages():
22
- """필요한 패키지 자동 설치."""
23
- packages = {
24
- "pdfminer.six": "pdfminer",
25
- "python-pptx": "pptx",
26
- "openpyxl": "openpyxl",
27
- }
28
- missing = []
29
- for pip_name, import_name in packages.items():
30
- try:
31
- __import__(import_name)
32
- except ImportError:
33
- missing.append(pip_name)
34
- if missing:
35
- print(f"패키지 설치 중: {', '.join(missing)}...", file=sys.stderr)
36
- subprocess.check_call(
37
- [sys.executable, "-m", "pip", "install", "-q", *missing],
38
- stdout=subprocess.DEVNULL,
39
- stderr=subprocess.DEVNULL,
40
- )
41
-
42
-
43
- ensure_packages()
44
-
45
- from pdfminer.high_level import extract_text as pdf_extract_text # noqa: E402
46
- from pptx import Presentation # noqa: E402
47
- from openpyxl import load_workbook # noqa: E402
48
-
49
-
50
- # ── Korean charset helpers ──────────────────────────────────────────
51
-
52
- KOREAN_CHARSET_MAP = {
53
- "ks_c_5601-1987": "cp949",
54
- "ks_c_5601": "cp949",
55
- "euc_kr": "cp949",
56
- "euc-kr": "cp949",
57
- }
58
-
59
-
60
- def fix_charset(charset: str) -> str:
61
- if charset is None:
62
- return "utf-8"
63
- return KOREAN_CHARSET_MAP.get(charset.lower(), charset)
64
-
65
-
66
- # ── EML parsing ─────────────────────────────────────────────────────
67
-
68
-
69
- def parse_eml(filepath: str):
70
- with open(filepath, "rb") as f:
71
- msg = email.message_from_binary_file(f, policy=default_policy)
72
-
73
- # Headers
74
- headers = {
75
- "subject": str(msg["Subject"] or ""),
76
- "from": str(msg["From"] or ""),
77
- "to": str(msg["To"] or ""),
78
- "cc": str(msg["Cc"] or ""),
79
- "date": str(msg["Date"] or ""),
80
- }
81
-
82
- # Body
83
- body_plain = ""
84
- body_html = ""
85
-
86
- if msg.is_multipart():
87
- for part in msg.walk():
88
- ctype = part.get_content_type()
89
- cdisp = part.get_content_disposition()
90
- if cdisp == "attachment":
91
- continue
92
- if ctype == "text/plain" and not body_plain:
93
- body_plain = _get_text(part)
94
- elif ctype == "text/html" and not body_html:
95
- body_html = _get_text(part)
96
- else:
97
- body_plain = _get_text(msg)
98
-
99
- # Attachments
100
- attachments = []
101
- for part in msg.walk():
102
- filename = part.get_filename()
103
- if not filename:
104
- continue
105
- cdisp = part.get_content_disposition()
106
- if cdisp not in ("attachment", "inline", None):
107
- continue
108
- payload = part.get_payload(decode=True)
109
- if payload is None:
110
- continue
111
- attachments.append(
112
- {
113
- "filename": filename,
114
- "content_type": part.get_content_type(),
115
- "size": len(payload),
116
- "data": payload,
117
- }
118
- )
119
-
120
- return headers, body_plain, body_html, attachments
121
-
122
-
123
- def _get_text(part) -> str:
124
- try:
125
- return part.get_content()
126
- except Exception:
127
- payload = part.get_payload(decode=True)
128
- if not payload:
129
- return ""
130
- charset = fix_charset(part.get_content_charset())
131
- return payload.decode(charset, errors="replace")
132
-
133
-
134
- # ── Attachment extractors ───────────────────────────────────────────
135
-
136
-
137
- def extract_pdf(data: bytes) -> str:
138
- with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f:
139
- f.write(data)
140
- tmp = f.name
141
- try:
142
- text = pdf_extract_text(tmp)
143
- return text.strip() if text else "(텍스트 추출 실패)"
144
- except Exception as e:
145
- return f"(PDF 파싱 오류: {e})"
146
- finally:
147
- os.unlink(tmp)
148
-
149
-
150
- def extract_pptx(data: bytes) -> str:
151
- with tempfile.NamedTemporaryFile(suffix=".pptx", delete=False) as f:
152
- f.write(data)
153
- tmp = f.name
154
- try:
155
- prs = Presentation(tmp)
156
- slides = []
157
- for i, slide in enumerate(prs.slides, 1):
158
- lines = [f"#### 슬라이드 {i}"]
159
- for shape in slide.shapes:
160
- if shape.has_text_frame:
161
- for para in shape.text_frame.paragraphs:
162
- line = "".join(run.text for run in para.runs)
163
- if line.strip():
164
- lines.append(line)
165
- if shape.has_table:
166
- header = " | ".join(
167
- cell.text for cell in shape.table.rows[0].cells
168
- )
169
- sep = " | ".join(
170
- "---" for _ in shape.table.rows[0].cells
171
- )
172
- lines.append(f"| {header} |")
173
- lines.append(f"| {sep} |")
174
- for row in list(shape.table.rows)[1:]:
175
- row_text = " | ".join(cell.text for cell in row.cells)
176
- lines.append(f"| {row_text} |")
177
- if slide.has_notes_slide:
178
- notes = slide.notes_slide.notes_text_frame.text
179
- if notes.strip():
180
- lines.append(f"> 노트: {notes}")
181
- slides.append("\n".join(lines))
182
- return "\n\n".join(slides) if slides else "(텍스트 없음)"
183
- except Exception as e:
184
- return f"(PPTX 파싱 오류: {e})"
185
- finally:
186
- os.unlink(tmp)
187
-
188
-
189
- def extract_xlsx(data: bytes) -> str:
190
- with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as f:
191
- f.write(data)
192
- tmp = f.name
193
- try:
194
- wb = load_workbook(tmp, data_only=True)
195
- sheets = []
196
- for name in wb.sheetnames:
197
- ws = wb[name]
198
- lines = [f"#### 시트: {name}"]
199
- rows = list(ws.iter_rows(values_only=True))
200
- if not rows:
201
- lines.append("(데이터 없음)")
202
- sheets.append("\n".join(lines))
203
- continue
204
- # 마크다운 테이블
205
- first_row = rows[0]
206
- col_count = len(first_row)
207
- header = " | ".join(str(c) if c is not None else "" for c in first_row)
208
- sep = " | ".join("---" for _ in range(col_count))
209
- lines.append(f"| {header} |")
210
- lines.append(f"| {sep} |")
211
- for row in rows[1:]:
212
- vals = " | ".join(str(c) if c is not None else "" for c in row)
213
- if any(c is not None for c in row):
214
- lines.append(f"| {vals} |")
215
- sheets.append("\n".join(lines))
216
- return "\n\n".join(sheets) if sheets else "(데이터 없음)"
217
- except Exception as e:
218
- return f"(XLSX 파싱 오류: {e})"
219
- finally:
220
- os.unlink(tmp)
221
-
222
-
223
- def extract_text_file(data: bytes) -> str:
224
- for enc in ("utf-8", "cp949", "euc-kr", "latin-1"):
225
- try:
226
- return data.decode(enc)
227
- except (UnicodeDecodeError, LookupError):
228
- continue
229
- return data.decode("utf-8", errors="replace")
230
-
231
-
232
- # ── HTML stripping ──────────────────────────────────────────────────
233
-
234
-
235
- def strip_html(text: str) -> str:
236
- text = re.sub(r"<style[^>]*>.*?</style>", "", text, flags=re.DOTALL | re.I)
237
- text = re.sub(r"<script[^>]*>.*?</script>", "", text, flags=re.DOTALL | re.I)
238
- text = re.sub(r"<br\s*/?>", "\n", text, flags=re.I)
239
- text = re.sub(r"</(?:p|div|tr|li)>", "\n", text, flags=re.I)
240
- text = re.sub(r"<[^>]+>", "", text)
241
- text = html.unescape(text)
242
- text = re.sub(r"\n{3,}", "\n\n", text)
243
- return text.strip()
244
-
245
-
246
- # ── Size formatting ─────────────────────────────────────────────────
247
-
248
-
249
- def fmt_size(n: int) -> str:
250
- if n < 1024:
251
- return f"{n} B"
252
- if n < 1024 * 1024:
253
- return f"{n / 1024:.1f} KB"
254
- return f"{n / (1024 * 1024):.1f} MB"
255
-
256
-
257
- # ── Markdown report ─────────────────────────────────────────────────
258
-
259
- PARSEABLE_EXTS = {".pdf", ".xlsx", ".xls", ".pptx", ".txt", ".csv", ".log", ".json", ".xml", ".html", ".htm", ".md"}
260
- IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg"}
261
-
262
-
263
- def build_report(filepath: str) -> str:
264
- headers, body_plain, body_html, attachments = parse_eml(filepath)
265
-
266
- out = []
267
- out.append("# 이메일 분석서\n")
268
- out.append(f"**원본 파일**: `{os.path.basename(filepath)}`\n")
269
-
270
- # ── 메일 정보
271
- out.append("## 메일 정보\n")
272
- out.append("| 항목 | 내용 |")
273
- out.append("|------|------|")
274
- out.append(f"| **제목** | {headers['subject']} |")
275
- out.append(f"| **보낸 사람** | {headers['from']} |")
276
- out.append(f"| **받는 사람** | {headers['to']} |")
277
- if headers["cc"]:
278
- out.append(f"| **참조** | {headers['cc']} |")
279
- out.append(f"| **날짜** | {headers['date']} |")
280
- out.append(f"| **첨부파일** | {len(attachments)}개 |")
281
- out.append("")
282
-
283
- # ── 본문
284
- out.append("## 본문 내용\n")
285
- body = body_plain
286
- if not body and body_html:
287
- body = strip_html(body_html)
288
- out.append(body.strip() if body else "_(본문 없음)_")
289
- out.append("")
290
-
291
- # ── 첨부파일
292
- if attachments:
293
- out.append("## 첨부파일 분석\n")
294
- out.append("| # | 파일명 | 형식 | 크기 |")
295
- out.append("|---|--------|------|------|")
296
- for i, a in enumerate(attachments, 1):
297
- out.append(f"| {i} | {a['filename']} | {a['content_type']} | {fmt_size(a['size'])} |")
298
- out.append("")
299
-
300
- for i, a in enumerate(attachments, 1):
301
- ext = Path(a["filename"]).suffix.lower()
302
- out.append(f"### 첨부 {i}: {a['filename']}\n")
303
-
304
- if ext == ".pdf":
305
- out.append(extract_pdf(a["data"]))
306
- elif ext in (".xlsx", ".xls"):
307
- out.append(extract_xlsx(a["data"]))
308
- elif ext == ".pptx":
309
- out.append(extract_pptx(a["data"]))
310
- elif ext == ".ppt":
311
- out.append("_(.ppt 레거시 형식 미지원, .pptx만 지원)_")
312
- elif ext in (".txt", ".csv", ".log", ".json", ".xml", ".html", ".htm", ".md"):
313
- out.append(f"```\n{extract_text_file(a['data'])}\n```")
314
- elif ext in IMAGE_EXTS:
315
- out.append(f"_(이미지 파일 - {fmt_size(a['size'])})_")
316
- else:
317
- out.append(f"_(지원하지 않는 형식: {ext}, {fmt_size(a['size'])})_")
318
- out.append("")
319
-
320
- return "\n".join(out)
321
-
322
-
323
- # ── Main ────────────────────────────────────────────────────────────
324
-
325
- if __name__ == "__main__":
326
- if len(sys.argv) < 2:
327
- print("Usage: python eml-analyzer.py <eml_file_path>", file=sys.stderr)
328
- sys.exit(1)
329
-
330
- path = sys.argv[1]
331
- if not os.path.isfile(path):
332
- print(f"파일을 찾을 수 없습니다: {path}", file=sys.stderr)
333
- sys.exit(1)
334
-
335
- print(build_report(path))
@@ -1,2 +0,0 @@
1
- export declare function runNpx(args: string[]): void;
2
- //# sourceMappingURL=npx.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"npx.d.ts","sourceRoot":"","sources":["../../src/commands/npx.ts"],"names":[],"mappings":"AAMA,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAY3C"}
@@ -1,16 +0,0 @@
1
- import { spawn } from "child_process";
2
- function runNpx(args) {
3
- const command = process.platform === "win32" ? "npx.cmd" : "npx";
4
- const child = spawn(command, args, {
5
- stdio: "inherit",
6
- env: process.env,
7
- shell: true
8
- });
9
- child.on("exit", (code) => {
10
- process.exit(code ?? 1);
11
- });
12
- }
13
- export {
14
- runNpx
15
- };
16
- //# sourceMappingURL=npx.js.map
@@ -1,6 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/commands/npx.ts"],
4
- "mappings": "AAIA,SAAS,aAAa;AAEf,SAAS,OAAO,MAAsB;AAC3C,QAAM,UAAU,QAAQ,aAAa,UAAU,YAAY;AAE3D,QAAM,QAAQ,MAAM,SAAS,MAAM;AAAA,IACjC,OAAO;AAAA,IACP,KAAK,QAAQ;AAAA,IACb,OAAO;AAAA,EACT,CAAC;AAED,QAAM,GAAG,QAAQ,CAAC,SAAS;AACzB,YAAQ,KAAK,QAAQ,CAAC;AAAA,EACxB,CAAC;AACH;",
5
- "names": []
6
- }
@@ -1,19 +0,0 @@
1
- /**
2
- * 크로스플랫폼 npx 래퍼.
3
- * Windows에서는 npx.cmd, 그 외에서는 npx를 실행한다.
4
- */
5
- import { spawn } from "child_process";
6
-
7
- export function runNpx(args: string[]): void {
8
- const command = process.platform === "win32" ? "npx.cmd" : "npx";
9
-
10
- const child = spawn(command, args, {
11
- stdio: "inherit",
12
- env: process.env,
13
- shell: true,
14
- });
15
-
16
- child.on("exit", (code) => {
17
- process.exit(code ?? 1);
18
- });
19
- }