skilldrop-cli 0.16.0 → 0.16.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skilldrop-cli",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Portable AI-agent skills for the deliverables knowledge workers ship \u2014 diagrams, ADRs, PRDs, runbooks, decks, reviews \u2014 installable into Claude Code, Cursor, Kiro, and any IDE that takes custom instructions. One command per skill or per role-based pack.",
5
5
  "author": {
6
6
  "name": "Sanjay Ananthanarayan",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "file-to-markdown",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Convert a Word document, PowerPoint deck, Excel workbook, web page, CSV, JSON, text file or PDF into clean Markdown that keeps headings, lists, tables, links, bold and italic, slide titles and speaker notes, and reports everything it dropped (images, charts, comments, formulas, tracked changes), so the content can be read, reviewed or fed to another skill. Use when the user hands over a .docx, .pptx, .xlsx, .html, .csv, .json or .pdf and says \"read this\", \"turn this into markdown\", \"extract the text\", or wants it critiqued or briefed.",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -56,6 +56,7 @@ class Report:
56
56
  def md_escape(text):
57
57
  text = text.replace("\\", "\\\\")
58
58
  text = re.sub(r"([*_`\[\]])", r"\\\1", text)
59
+ text = re.sub(r"<(?=[A-Za-z/!])", r"\\<", text) # literal <tag> text must not read as raw HTML
59
60
  return text
60
61
 
61
62
 
@@ -330,7 +331,7 @@ def docx_to_md(path, rep):
330
331
  if tcpr is not None and tcpr.find(W + "gridSpan") is not None:
331
332
  row.extend([""] * (int(tcpr.find(W + "gridSpan").get(W + "val", "1")) - 1))
332
333
  rows.append(row)
333
- if any(tbl.iter(W + "tbl")) and len(list(tbl.iter(W + "tbl"))) > 1:
334
+ if len(list(tbl.iter(W + "tbl"))) > 1:
334
335
  rep.notes.append("a nested table was flattened into its parent cell")
335
336
  rep.keep("tables")
336
337
  return md_table(rows)
@@ -339,6 +340,19 @@ def docx_to_md(path, rep):
339
340
  return "".join((n.text or "") if n.tag == W + "t" else "\t" for n in p.iter()
340
341
  if n.tag in (W + "t", W + "tab"))
341
342
 
343
+ def style_name(p):
344
+ ppr = p.find(W + "pPr")
345
+ ps = ppr.find(W + "pStyle") if ppr is not None else None
346
+ if ps is None:
347
+ return ""
348
+ sid = ps.get(W + "val", "")
349
+ return styles.get(sid, (sid.lower(), None))[0]
350
+
351
+ def code_text(p):
352
+ """Text of a code paragraph: line breaks and tabs kept, no inline formatting or escaping."""
353
+ return "".join((n.text or "") if n.tag == W + "t" else "\t" if n.tag == W + "tab" else "\n"
354
+ for n in p.iter() if n.tag in (W + "t", W + "tab", W + "br", W + "cr"))
355
+
342
356
  def run_size(p):
343
357
  """(largest font size in half-points, every text run bold?) for unstyled documents."""
344
358
  sizes, bold = [0], True
@@ -402,7 +416,19 @@ def docx_to_md(path, rep):
402
416
  if not text:
403
417
  prev_list = False if not li else prev_list
404
418
  continue
405
- if lvl:
419
+ sname = "" if lvl or li else style_name(el)
420
+ if re.match(r"(source ?code|code|html preformatted|preformatted text|macro text)$", sname):
421
+ # one code block may be one paragraph with line breaks or a run of one-line paragraphs
422
+ if blocks and blocks[-1][0] == "code":
423
+ blocks[-1] = ("code", blocks[-1][1] + "\n" + code_text(el))
424
+ else:
425
+ blocks.append(("code", code_text(el)))
426
+ rep.keep("code blocks")
427
+ prev_list = False
428
+ elif "quote" in sname or sname == "block text":
429
+ blocks.append(("block", "> " + text.replace("\n", "\n> ")))
430
+ prev_list = False
431
+ elif lvl:
406
432
  rep.keep("headings")
407
433
  blocks.append(("block", "#" * lvl + " " + text.replace(" \n", " ")))
408
434
  prev_list = False
@@ -439,6 +465,11 @@ def docx_to_md(path, rep):
439
465
  for k, (kind, text) in enumerate(blocks):
440
466
  if k and not (kind == "list" and blocks[k - 1][0] == "list"):
441
467
  out.append("")
468
+ if kind == "code":
469
+ fence = "```"
470
+ while fence in text:
471
+ fence += "`"
472
+ text = fence + "\n" + text + "\n" + fence
442
473
  out.append(text)
443
474
  return "\n".join(out) + "\n"
444
475
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md-to-docx",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Turn a Markdown draft into a Word document (.docx) that someone outside engineering can open, edit and track changes in, with real Heading styles so the navigation pane and table of contents work, live hyperlinks, numbered and bulleted lists, tables, code blocks, embedded images and optional brand fonts and colours, and a warning for everything that could not be carried over. Use when the user wants a Markdown file as a Word doc, says \"send this as a .docx\", \"convert to Word\", \"legal needs it in Word\", or \"make this editable for the client\".",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -34,6 +34,8 @@ WARNINGS = []
34
34
 
35
35
  def warn(line, msg):
36
36
  where = "line %d: " % line if line else ""
37
+ if where + msg in WARNINGS: # a big table repeats one warning per cell; say it once
38
+ return
37
39
  WARNINGS.append(where + msg)
38
40
  print("WARN %s%s" % (where, msg), file=sys.stderr)
39
41
 
@@ -942,7 +944,7 @@ class Doc(object):
942
944
 
943
945
 
944
946
  def static_parts(title):
945
- now = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
947
+ now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
946
948
  ct = ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
947
949
  '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
948
950
  '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
@@ -78,7 +78,8 @@ nothing. Formulas stay off unless the request says otherwise, noted as an `[assu
78
78
  | Anything else, including `n/a`, `1.234,50`, `30/09/2026` | Text |
79
79
 
80
80
  In Markdown cells, `**bold**`, `` `code` ``, links and images are reduced to their text, and
81
- `<br>` becomes a line break inside a wrapped cell. JSON numbers and booleans keep their type.
81
+ `<br>` becomes a line break inside a wrapped cell. JSON numbers and booleans keep their type,
82
+ except integers over 15 digits, which become text.
82
83
  JSON strings stay text except ISO dates, because whoever produced the JSON chose a string.
83
84
 
84
85
  ## Useful references in this skill
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md-to-xlsx",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Turn the tables in a Markdown file, a CSV or a JSON array into an Excel workbook (.xlsx) that finance, ops or a client can sort, filter and sum, with one sheet per table named from its heading, real numbers, percentages, currency and dates instead of text, a bold frozen header row with filters, sensible column widths, and formulas left inert unless asked for. Use when the user wants a spreadsheet from Markdown tables or data, says \"put this in Excel\", \"convert the CSV to xlsx\", \"give finance a spreadsheet\", or \"turn this JSON into a sheet\".",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -170,8 +170,11 @@ def type_text(s, formulas, stats, tz_flag, infer_numbers=True):
170
170
  text = "0"
171
171
  return Cell("n", text, fmt, width)
172
172
  if RE_SCI.match(s):
173
- stats["numbers"] += 1
174
- return Cell("n", repr(float(s)), None, width)
173
+ val = float(s)
174
+ # 1e999 overflows to inf, which is not a number Excel can read: keep it as text
175
+ if val == val and val not in (float("inf"), float("-inf")):
176
+ stats["numbers"] += 1
177
+ return Cell("n", num_text(val), None, width)
175
178
  if len(raw) > MAX_CELL:
176
179
  stats["truncated"] += 1
177
180
  s = s[:MAX_CELL]
@@ -189,6 +192,10 @@ def type_json(v, formulas, stats, tz_flag):
189
192
  if v != v or v in (float("inf"), float("-inf")):
190
193
  stats["text"] += 1
191
194
  return Cell("s", str(v), None, len(str(v)))
195
+ if isinstance(v, int) and len(str(abs(v))) > 15:
196
+ # Excel keeps 15 significant digits; a longer integer is an ID, and rounding it is data loss
197
+ stats["text"] += 1
198
+ return Cell("s", str(v), None, len(str(v)))
192
199
  stats["numbers"] += 1
193
200
  return Cell("n", num_text(v), None, len(num_text(v)))
194
201
  if isinstance(v, (dict, list)):
@@ -230,6 +237,9 @@ def split_row(line):
230
237
  return cells
231
238
 
232
239
 
240
+ HTML_TAGS = [0] # tags removed by strip_md, reported once per file
241
+
242
+
233
243
  def strip_md(s):
234
244
  """Plain text from a Markdown table cell."""
235
245
  s = re.sub(r"<br\s*/?>", "\n", s, flags=re.I)
@@ -240,7 +250,8 @@ def strip_md(s):
240
250
  s = re.sub(r"(?<![\w*])\*(?!\s)(.+?)(?<!\s)\*(?![\w*])", r"\1", s)
241
251
  s = re.sub(r"(?<!\w)_(?!\s)(.+?)(?<!\s)_(?!\w)", r"\1", s)
242
252
  s = re.sub(r"~~(.+?)~~", r"\1", s)
243
- s = re.sub(r"</?[A-Za-z][^>]*>", "", s)
253
+ s, n = re.subn(r"</?[A-Za-z][^>]*>", "", s)
254
+ HTML_TAGS[0] += n
244
255
  s = re.sub(r"\\([\\`*_{}\[\]()#+\-.!|~])", r"\1", s)
245
256
  return s
246
257
 
@@ -280,6 +291,9 @@ def read_markdown(text):
280
291
  tables.append((heading, header, rows, start))
281
292
  continue
282
293
  i += 1
294
+ if HTML_TAGS[0]:
295
+ warn("%d HTML tag(s) removed from headings and cells (text between them kept); write a literal "
296
+ "<...> as &lt;...&gt; or in backticks to keep it" % HTML_TAGS[0])
283
297
  return tables
284
298
 
285
299
 
@@ -533,7 +547,7 @@ def write_xlsx(out, sheets, title, formulas):
533
547
  '<bookViews><workbookView activeTab="0"/></bookViews><sheets>%s</sheets>'
534
548
  '<definedNames>%s</definedNames>%s</workbook>') % (
535
549
  "".join(sheet_tags), "".join(names), '<calcPr fullCalcOnLoad="1"/>' if formulas else "")
536
- now = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
550
+ now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
537
551
  core = ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
538
552
  '<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" '
539
553
  'xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" '
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pre-merge-review",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Gate an existing change for merge — run one skill that fires the whole production-readiness pass. First a deterministic mechanical gate (lint + typecheck + tests, via a script whose exit code decides), then the three-reviewer panel dispatched in parallel — devils-advocate (correctness), security-reviewer (exploitability), code-quality (craft) — then a single READY / NOT READY verdict. Use before opening a PR or merging, when you want the full review fleet run on a diff you already have. Do NOT use to implement a feature from a spec (that's feature-implement-loop) or to review a document (that's doc-critique).",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": { "npm": [], "pip": [] },
@@ -112,7 +112,10 @@ def install_hook(root, cmd, config):
112
112
  body += "\n" + _hook_block(cmd, config)
113
113
  with open(path, "w", encoding="utf-8") as fh:
114
114
  fh.write(body)
115
- os.chmod(path, 0o755)
115
+ # chmod +x, not a fixed 0o755: add execute only where the umask already granted read, so a
116
+ # restrictive umask (077) keeps the hook private instead of being widened to world-readable.
117
+ mode = os.stat(path).st_mode
118
+ os.chmod(path, mode | ((mode & 0o444) >> 2))
116
119
  print(f"gate: installed a blocking pre-commit hook -> {path}")
117
120
  print(" a RED gate now blocks `git commit` (any tool). Bypass once with --no-verify; "
118
121
  "wire CI required-checks for the un-bypassable backstop.")