@tasksai/install 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +5 -3
  2. package/runtime/server.py +250 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tasksai/install",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Shared TasksAI MCP installer CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "files": [
10
10
  "src/",
11
- "runtime/",
11
+ "runtime/server.py",
12
+ "runtime/requirements.txt",
12
13
  "README.md"
13
14
  ],
14
15
  "keywords": [
@@ -29,6 +30,7 @@
29
30
  "access": "public"
30
31
  },
31
32
  "scripts": {
32
- "check": "node --check src/index.js"
33
+ "check": "node --check src/index.js",
34
+ "test": "python3 -m unittest discover -s tests"
33
35
  }
34
36
  }
package/runtime/server.py CHANGED
@@ -92,7 +92,7 @@ if not LICENSE_KEY:
92
92
  print("Find your key in your purchase confirmation email.", file=sys.stderr, flush=True)
93
93
  sys.exit(1)
94
94
 
95
- SERVER_VERSION = "2.2.0"
95
+ SERVER_VERSION = "2.3.0"
96
96
 
97
97
  AUTH_HEADERS = {
98
98
  "Authorization": f"Bearer {LICENSE_KEY}",
@@ -630,25 +630,202 @@ def unique_output_path(root, filename, overwrite=False):
630
630
  raise RuntimeError("Could not choose a unique output filename.")
631
631
 
632
632
 
633
+ def markdown_table_row(line):
634
+ """Return parsed table cells when a line looks like a Markdown table row."""
635
+ stripped = line.strip()
636
+ if "|" not in stripped:
637
+ return None
638
+ if stripped.startswith("|"):
639
+ stripped = stripped[1:]
640
+ if stripped.endswith("|"):
641
+ stripped = stripped[:-1]
642
+ cells = []
643
+ current = []
644
+ escaped = False
645
+ for char in stripped:
646
+ if escaped:
647
+ current.append(char)
648
+ escaped = False
649
+ elif char == "\\":
650
+ escaped = True
651
+ elif char == "|":
652
+ cells.append("".join(current).strip())
653
+ current = []
654
+ else:
655
+ current.append(char)
656
+ cells.append("".join(current).strip())
657
+ return cells if len(cells) > 1 else None
658
+
659
+
660
+ def markdown_table_alignments(separator_cells):
661
+ """Return column alignments if cells are a valid Markdown table separator."""
662
+ alignments = []
663
+ for cell in separator_cells or []:
664
+ compact = cell.replace(" ", "")
665
+ if not re.match(r"^:?-{3,}:?$", compact):
666
+ return None
667
+ if compact.startswith(":") and compact.endswith(":"):
668
+ alignments.append("center")
669
+ elif compact.endswith(":"):
670
+ alignments.append("right")
671
+ else:
672
+ alignments.append("left")
673
+ return alignments
674
+
675
+
676
+ def is_markdown_table_start(lines, index):
677
+ """True when lines[index:index+2] form a Markdown table header."""
678
+ if index + 1 >= len(lines):
679
+ return False
680
+ header = markdown_table_row(lines[index])
681
+ separator = markdown_table_row(lines[index + 1])
682
+ return bool(header and markdown_table_alignments(separator))
683
+
684
+
685
+ def iter_inline_markdown(text):
686
+ """Yield (text, marks) spans for common inline Markdown."""
687
+ token_re = re.compile(
688
+ r"(`[^`]+`|\*\*[^*]+\*\*|__[^_]+__|\*[^*\s][^*]*\*|_[^_\s][^_]*_|\[[^\]]+\]\([^)]+\))"
689
+ )
690
+ pos = 0
691
+ for match in token_re.finditer(text):
692
+ if match.start() > pos:
693
+ yield text[pos:match.start()], {}
694
+ token = match.group(0)
695
+ marks = {}
696
+ value = token
697
+ if token.startswith("**") and token.endswith("**"):
698
+ value = token[2:-2]
699
+ marks["bold"] = True
700
+ elif token.startswith("__") and token.endswith("__"):
701
+ value = token[2:-2]
702
+ marks["bold"] = True
703
+ elif token.startswith("`") and token.endswith("`"):
704
+ value = token[1:-1]
705
+ marks["code"] = True
706
+ elif token.startswith("["):
707
+ link = re.match(r"^\[([^\]]+)\]\(([^)]+)\)$", token)
708
+ if link:
709
+ value = link.group(1)
710
+ marks["link"] = link.group(2)
711
+ elif token[0] in {"*", "_"} and token.endswith(token[0]):
712
+ value = token[1:-1]
713
+ marks["italic"] = True
714
+ yield value, marks
715
+ pos = match.end()
716
+ if pos < len(text):
717
+ yield text[pos:], {}
718
+
719
+
720
+ def add_hyperlink(paragraph, text, url):
721
+ """Add a clickable hyperlink run when python-docx low-level APIs are available."""
722
+ from docx.oxml import OxmlElement
723
+ from docx.oxml.ns import qn
724
+
725
+ part = paragraph.part
726
+ relationship_id = part.relate_to(
727
+ url,
728
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
729
+ is_external=True,
730
+ )
731
+
732
+ hyperlink = OxmlElement("w:hyperlink")
733
+ hyperlink.set(qn("r:id"), relationship_id)
734
+ run_element = OxmlElement("w:r")
735
+ properties = OxmlElement("w:rPr")
736
+
737
+ color = OxmlElement("w:color")
738
+ color.set(qn("w:val"), "0563C1")
739
+ properties.append(color)
740
+
741
+ underline = OxmlElement("w:u")
742
+ underline.set(qn("w:val"), "single")
743
+ properties.append(underline)
744
+
745
+ run_element.append(properties)
746
+ text_element = OxmlElement("w:t")
747
+ text_element.text = text
748
+ run_element.append(text_element)
749
+ hyperlink.append(run_element)
750
+ paragraph._p.append(hyperlink)
751
+
752
+
633
753
  def add_markdown_text(paragraph, text):
634
- """Add basic inline markdown formatting to a python-docx paragraph."""
635
- pattern = r"\*\*(.+?)\*\*"
636
- last_end = 0
637
- for match in re.finditer(pattern, text):
638
- if match.start() > last_end:
639
- paragraph.add_run(text[last_end:match.start()])
640
- bold_run = paragraph.add_run(match.group(1))
641
- bold_run.bold = True
642
- last_end = match.end()
643
- if last_end < len(text):
644
- paragraph.add_run(text[last_end:])
754
+ """Add common inline Markdown formatting to a python-docx paragraph."""
755
+ for value, marks in iter_inline_markdown(text):
756
+ if not value:
757
+ continue
758
+ if marks.get("link"):
759
+ try:
760
+ add_hyperlink(paragraph, value, marks["link"])
761
+ except Exception:
762
+ paragraph.add_run(f"{value} ({marks['link']})")
763
+ continue
764
+ run = paragraph.add_run(value)
765
+ if marks.get("bold"):
766
+ run.bold = True
767
+ if marks.get("italic"):
768
+ run.italic = True
769
+ if marks.get("code"):
770
+ run.font.name = "Courier New"
771
+
772
+
773
+ def add_horizontal_rule(paragraph):
774
+ """Render a horizontal Markdown rule as a Word paragraph border."""
775
+ from docx.oxml import OxmlElement
776
+ from docx.oxml.ns import qn
777
+
778
+ p_pr = paragraph._p.get_or_add_pPr()
779
+ borders = p_pr.first_child_found_in("w:pBdr")
780
+ if borders is None:
781
+ borders = OxmlElement("w:pBdr")
782
+ p_pr.append(borders)
783
+ bottom = OxmlElement("w:bottom")
784
+ bottom.set(qn("w:val"), "single")
785
+ bottom.set(qn("w:sz"), "6")
786
+ bottom.set(qn("w:space"), "1")
787
+ bottom.set(qn("w:color"), "BFBFBF")
788
+ borders.append(bottom)
789
+
790
+
791
+ def add_markdown_table(doc, rows, alignments):
792
+ """Render a Markdown pipe table as a native Word table."""
793
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
794
+ from docx.shared import Pt
795
+
796
+ if not rows:
797
+ return
798
+
799
+ width = max(len(row) for row in rows)
800
+ normalized = [row + [""] * (width - len(row)) for row in rows]
801
+ table = doc.add_table(rows=len(normalized), cols=width)
802
+ table.style = "Table Grid"
803
+ table.autofit = True
804
+
805
+ for row_index, row in enumerate(normalized):
806
+ for column_index, value in enumerate(row):
807
+ cell = table.cell(row_index, column_index)
808
+ paragraph = cell.paragraphs[0]
809
+ add_markdown_text(paragraph, value)
810
+ if row_index == 0:
811
+ for run in paragraph.runs:
812
+ run.bold = True
813
+ alignment = alignments[column_index] if column_index < len(alignments) else "left"
814
+ if alignment == "right":
815
+ paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
816
+ elif alignment == "center":
817
+ paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
818
+ for run in paragraph.runs:
819
+ run.font.size = Pt(8 if width > 6 else 9)
820
+
821
+ doc.add_paragraph()
645
822
 
646
823
 
647
824
  def write_docx(path, title, content_markdown, product_name):
648
825
  """Write a simple, polished DOCX file locally."""
649
826
  try:
650
827
  from docx import Document
651
- from docx.shared import Pt
828
+ from docx.shared import Inches, Pt
652
829
  except Exception as exc:
653
830
  raise RuntimeError(
654
831
  "python-docx is not installed. Re-run the official installer or install python-docx in the MCP runtime environment."
@@ -669,33 +846,86 @@ def write_docx(path, title, content_markdown, product_name):
669
846
  if text:
670
847
  add_markdown_text(doc.add_paragraph(), text)
671
848
 
672
- for raw_line in lines:
849
+ in_code_block = False
850
+ index = 0
851
+ while index < len(lines):
852
+ raw_line = lines[index]
673
853
  line = raw_line.rstrip()
674
854
  stripped = line.strip()
855
+ if stripped.startswith("```"):
856
+ flush_paragraph()
857
+ in_code_block = not in_code_block
858
+ index += 1
859
+ continue
860
+ if in_code_block:
861
+ para = doc.add_paragraph()
862
+ run = para.add_run(line)
863
+ run.font.name = "Courier New"
864
+ run.font.size = Pt(9)
865
+ index += 1
866
+ continue
675
867
  if not stripped:
676
868
  flush_paragraph()
869
+ index += 1
677
870
  continue
678
871
 
679
872
  heading = re.match(r"^(#{1,4})\s+(.+)$", stripped)
680
- bullet = re.match(r"^[-*]\s+(.+)$", stripped)
681
- numbered = re.match(r"^\d+\.\s+(.+)$", stripped)
873
+ bullet = re.match(r"^(\s*)[-*+]\s+(.+)$", line)
874
+ numbered = re.match(r"^(\s*)\d+\.\s+(.+)$", line)
875
+ quote = re.match(r"^>\s?(.+)$", stripped)
682
876
 
683
- if heading:
877
+ if is_markdown_table_start(lines, index):
878
+ flush_paragraph()
879
+ header = markdown_table_row(lines[index])
880
+ alignments = markdown_table_alignments(markdown_table_row(lines[index + 1]))
881
+ table_rows = [header]
882
+ index += 2
883
+ while index < len(lines):
884
+ row = markdown_table_row(lines[index])
885
+ if not row:
886
+ break
887
+ table_rows.append(row)
888
+ index += 1
889
+ add_markdown_table(doc, table_rows, alignments)
890
+ continue
891
+ elif heading:
684
892
  flush_paragraph()
685
893
  doc.add_heading(heading.group(2).strip(), level=min(len(heading.group(1)), 4))
894
+ index += 1
686
895
  elif bullet:
687
896
  flush_paragraph()
688
897
  para = doc.add_paragraph(style="List Bullet")
689
- add_markdown_text(para, bullet.group(1).strip())
898
+ indent_level = min(len(bullet.group(1).replace("\t", " ")) // 2, 4)
899
+ para.paragraph_format.left_indent = Inches(0.25 * indent_level)
900
+ text = bullet.group(2).strip()
901
+ checkbox = re.match(r"^\[([ xX])\]\s+(.+)$", text)
902
+ if checkbox:
903
+ text = f"[{checkbox.group(1).lower()}] {checkbox.group(2).strip()}"
904
+ add_markdown_text(para, text)
905
+ index += 1
690
906
  elif numbered:
691
907
  flush_paragraph()
692
908
  para = doc.add_paragraph(style="List Number")
693
- add_markdown_text(para, numbered.group(1).strip())
909
+ indent_level = min(len(numbered.group(1).replace("\t", " ")) // 2, 4)
910
+ para.paragraph_format.left_indent = Inches(0.25 * indent_level)
911
+ add_markdown_text(para, numbered.group(2).strip())
912
+ index += 1
913
+ elif quote:
914
+ flush_paragraph()
915
+ try:
916
+ para = doc.add_paragraph(style="Quote")
917
+ except Exception:
918
+ para = doc.add_paragraph()
919
+ para.paragraph_format.left_indent = Inches(0.25)
920
+ add_markdown_text(para, quote.group(1).strip())
921
+ index += 1
694
922
  elif stripped in {"---", "***", "___"}:
695
923
  flush_paragraph()
696
- doc.add_paragraph()
924
+ add_horizontal_rule(doc.add_paragraph())
925
+ index += 1
697
926
  else:
698
927
  paragraph_buffer.append(stripped)
928
+ index += 1
699
929
 
700
930
  flush_paragraph()
701
931