@tasksai/install 0.1.22 → 0.1.24

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": "@tasksai/install",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "Shared TasksAI MCP installer CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- mcp>=1.0.0
1
+ mcp>=1.0.0,<2.0.0
2
2
  httpx>=0.27.0
3
3
  python-dotenv>=1.0.0
4
4
  python-docx>=1.1.2
package/runtime/server.py CHANGED
@@ -28,6 +28,7 @@ import httpx
28
28
  import uuid
29
29
  from datetime import datetime
30
30
  from pathlib import Path
31
+ from urllib.parse import urlencode
31
32
 
32
33
  # The runtime is installed as a pair of adjacent Python files and is also
33
34
  # loaded directly by path in focused tests, so make the sibling matcher
@@ -124,7 +125,7 @@ if not LICENSE_KEY:
124
125
  print("Find your key in your purchase confirmation email.", file=sys.stderr, flush=True)
125
126
  sys.exit(1)
126
127
 
127
- SERVER_VERSION = "2.3.0"
128
+ SERVER_VERSION = "2.4.0"
128
129
 
129
130
  AUTH_HEADERS = {
130
131
  "Authorization": f"Bearer {LICENSE_KEY}",
@@ -519,6 +520,79 @@ async def api_post(path, payload):
519
520
  return resp.json()
520
521
 
521
522
 
523
+ def jurisdiction_sources_path(arguments):
524
+ """Build a metadata-only source lookup path without customer or property data."""
525
+ skill_id = (arguments.get("skill_id") or "").strip()
526
+ state = (arguments.get("state") or "").strip()
527
+ if not skill_id:
528
+ raise ValueError("skill_id is required.")
529
+
530
+ params = {"skill_id": skill_id}
531
+ if state:
532
+ params["state"] = state
533
+ for field in ("county", "locality", "district"):
534
+ value = (arguments.get(field) or "").strip()
535
+ if value:
536
+ params[field] = value
537
+ return f"/v1/skills/jurisdiction-sources?{urlencode(params)}"
538
+
539
+
540
+ def format_jurisdiction_source_pack(pack):
541
+ """Render the public source pack as compact instructions for the local AI."""
542
+ status = pack.get("status", "unsupported")
543
+ lines = [
544
+ "# Official Jurisdiction and Authority Source Pack",
545
+ "",
546
+ f"- Status: **{status}**",
547
+ f"- Coverage: **{pack.get('coverage_level', 'intake_only')}**",
548
+ f"- Skill: `{pack.get('skill_id', '')}`",
549
+ f"- Registry reviewed: {pack.get('registry_reviewed_on', 'not supplied')}",
550
+ f"- Registry refresh due: {pack.get('registry_refresh_due_on', 'not supplied')}",
551
+ ]
552
+ if pack.get("state_name"):
553
+ lines.append(f"- State: {pack['state_name']} ({pack.get('state_code', '')})")
554
+ if pack.get("county"):
555
+ lines.append(f"- County: {pack['county']}")
556
+ if pack.get("locality"):
557
+ lines.append(f"- Locality: {pack['locality']}")
558
+ if pack.get("district"):
559
+ lines.append(f"- District: {pack['district']}")
560
+ authority_layers = pack.get("authority_layers") or []
561
+ if authority_layers:
562
+ lines.append(f"- Authority layers: {', '.join(authority_layers)}")
563
+
564
+ sources = pack.get("sources") or []
565
+ if sources:
566
+ lines.extend(["", "## Sources"])
567
+ for source in sources:
568
+ effective = source.get("effective_date") or "not specified"
569
+ lines.extend(
570
+ [
571
+ "",
572
+ f"### {source.get('title', 'Official source')}",
573
+ f"- Authority: {source.get('authority', '')}",
574
+ f"- Jurisdiction: {source.get('jurisdiction', '')} ({source.get('scope_type', 'scope not supplied')})",
575
+ f"- Effective date: {effective}",
576
+ f"- Reviewed: {source.get('reviewed_on', '')}",
577
+ f"- Refresh due: {source.get('refresh_due_on', '')}",
578
+ f"- URL: {source.get('url', '')}",
579
+ f"- Use: {source.get('purpose', '')}",
580
+ f"- Limit: {source.get('limits', '')}",
581
+ ]
582
+ )
583
+
584
+ required_authorities = pack.get("required_user_authorities") or []
585
+ if required_authorities:
586
+ lines.extend(["", "## Professional-Supplied Authority Still Needed"])
587
+ lines.extend(f"- {item}" for item in required_authorities)
588
+
589
+ instructions = pack.get("instructions") or []
590
+ if instructions:
591
+ lines.extend(["", "## Required handling"])
592
+ lines.extend(f"- {instruction}" for instruction in instructions)
593
+ return "\n".join(lines)
594
+
595
+
522
596
  def get_install_id() -> str:
523
597
  """Stable anonymous install id for attribution; stored locally only."""
524
598
  global INSTALL_ID
@@ -828,6 +902,53 @@ def is_markdown_table_start(lines, index):
828
902
  return bool(header and markdown_table_alignments(separator))
829
903
 
830
904
 
905
+ def inferred_plain_heading_level(lines, index):
906
+ """Infer conservative headings when an AI omits Markdown # markers."""
907
+ text = lines[index].strip()
908
+ if not text or len(text) > 90 or "|" in text or ":" in text:
909
+ return None
910
+ if text.endswith((".", "?", "!", ";")):
911
+ return None
912
+
913
+ letters = [char for char in text if char.isalpha()]
914
+ if len(letters) >= 4 and all(char.isupper() for char in letters):
915
+ return 2
916
+
917
+ next_index = index + 1
918
+ while next_index < len(lines) and not lines[next_index].strip():
919
+ next_index += 1
920
+ next_line = lines[next_index] if next_index < len(lines) else ""
921
+ introduces_list = bool(
922
+ re.match(r"^\s*[-*+]\s+\S", next_line)
923
+ or re.match(r"^\s*\d+\.\s+\S", next_line)
924
+ )
925
+ introduces_facts = bool(re.match(r"^\s*[^:\n]{1,55}:\s+\S", next_line))
926
+ if introduces_list or introduces_facts or text.istitle():
927
+ return 3
928
+ return None
929
+
930
+
931
+ def labeled_fact(line):
932
+ """Return a label/value pair for a short professional fact line."""
933
+ match = re.match(r"^([^:\n]{1,55}):\s+(.+)$", line.strip())
934
+ if not match:
935
+ return None
936
+ return match.group(1).strip(), match.group(2).strip()
937
+
938
+
939
+ def add_labeled_text(paragraph, label, value):
940
+ """Render a label/value pair in an existing Word paragraph."""
941
+ label_run = paragraph.add_run(f"{label}: ")
942
+ label_run.bold = True
943
+ add_markdown_text(paragraph, value)
944
+
945
+
946
+ def add_labeled_fact(doc, label, value):
947
+ """Render a fact line with a bold label and distinct Word paragraph."""
948
+ paragraph = doc.add_paragraph()
949
+ add_labeled_text(paragraph, label, value)
950
+
951
+
831
952
  def iter_inline_markdown(text):
832
953
  """Yield (text, marks) spans for common inline Markdown."""
833
954
  token_re = re.compile(
@@ -934,10 +1055,81 @@ def add_horizontal_rule(paragraph):
934
1055
  borders.append(bottom)
935
1056
 
936
1057
 
1058
+ def set_cell_shading(cell, fill):
1059
+ """Apply a solid background color to a Word table cell."""
1060
+ from docx.oxml import OxmlElement
1061
+ from docx.oxml.ns import qn
1062
+
1063
+ cell_properties = cell._tc.get_or_add_tcPr()
1064
+ shading = cell_properties.find(qn("w:shd"))
1065
+ if shading is None:
1066
+ shading = OxmlElement("w:shd")
1067
+ cell_properties.append(shading)
1068
+ shading.set(qn("w:fill"), fill)
1069
+
1070
+
1071
+ def set_cell_margins(cell, top=70, start=90, bottom=70, end=90):
1072
+ """Set compact, readable table cell margins in twentieths of a point."""
1073
+ from docx.oxml import OxmlElement
1074
+ from docx.oxml.ns import qn
1075
+
1076
+ cell_properties = cell._tc.get_or_add_tcPr()
1077
+ margins = cell_properties.first_child_found_in("w:tcMar")
1078
+ if margins is None:
1079
+ margins = OxmlElement("w:tcMar")
1080
+ cell_properties.append(margins)
1081
+ for name, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
1082
+ node = margins.find(qn(f"w:{name}"))
1083
+ if node is None:
1084
+ node = OxmlElement(f"w:{name}")
1085
+ margins.append(node)
1086
+ node.set(qn("w:w"), str(value))
1087
+ node.set(qn("w:type"), "dxa")
1088
+
1089
+
1090
+ def repeat_table_header(row):
1091
+ """Repeat the first row of a long table on subsequent pages."""
1092
+ from docx.oxml import OxmlElement
1093
+ from docx.oxml.ns import qn
1094
+
1095
+ row_properties = row._tr.get_or_add_trPr()
1096
+ repeat = row_properties.find(qn("w:tblHeader"))
1097
+ if repeat is None:
1098
+ repeat = OxmlElement("w:tblHeader")
1099
+ row_properties.append(repeat)
1100
+ repeat.set(qn("w:val"), "true")
1101
+
1102
+
1103
+ def prevent_table_row_split(row):
1104
+ """Keep a table row together when Word paginates the document."""
1105
+ from docx.oxml import OxmlElement
1106
+ from docx.oxml.ns import qn
1107
+
1108
+ row_properties = row._tr.get_or_add_trPr()
1109
+ if row_properties.find(qn("w:cantSplit")) is None:
1110
+ row_properties.append(OxmlElement("w:cantSplit"))
1111
+
1112
+
1113
+ def add_page_number(paragraph):
1114
+ """Add a dynamic PAGE field to a footer paragraph."""
1115
+ from docx.oxml import OxmlElement
1116
+ from docx.oxml.ns import qn
1117
+
1118
+ run = paragraph.add_run()
1119
+ begin = OxmlElement("w:fldChar")
1120
+ begin.set(qn("w:fldCharType"), "begin")
1121
+ instruction = OxmlElement("w:instrText")
1122
+ instruction.set(qn("xml:space"), "preserve")
1123
+ instruction.text = " PAGE "
1124
+ end = OxmlElement("w:fldChar")
1125
+ end.set(qn("w:fldCharType"), "end")
1126
+ run._r.extend((begin, instruction, end))
1127
+
1128
+
937
1129
  def add_markdown_table(doc, rows, alignments):
938
1130
  """Render a Markdown pipe table as a native Word table."""
939
1131
  from docx.enum.text import WD_ALIGN_PARAGRAPH
940
- from docx.shared import Pt
1132
+ from docx.shared import Pt, RGBColor
941
1133
 
942
1134
  if not rows:
943
1135
  return
@@ -947,15 +1139,25 @@ def add_markdown_table(doc, rows, alignments):
947
1139
  table = doc.add_table(rows=len(normalized), cols=width)
948
1140
  table.style = "Table Grid"
949
1141
  table.autofit = True
1142
+ repeat_table_header(table.rows[0])
1143
+ for table_row in table.rows:
1144
+ prevent_table_row_split(table_row)
950
1145
 
951
1146
  for row_index, row in enumerate(normalized):
952
1147
  for column_index, value in enumerate(row):
953
1148
  cell = table.cell(row_index, column_index)
1149
+ set_cell_margins(cell)
1150
+ if row_index == 0:
1151
+ set_cell_shading(cell, "17365D")
1152
+ elif row_index % 2 == 0:
1153
+ set_cell_shading(cell, "EEF3F8")
954
1154
  paragraph = cell.paragraphs[0]
1155
+ paragraph.paragraph_format.space_after = Pt(0)
955
1156
  add_markdown_text(paragraph, value)
956
1157
  if row_index == 0:
957
1158
  for run in paragraph.runs:
958
1159
  run.bold = True
1160
+ run.font.color.rgb = RGBColor(255, 255, 255)
959
1161
  alignment = alignments[column_index] if column_index < len(alignments) else "left"
960
1162
  if alignment == "right":
961
1163
  paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
@@ -968,16 +1170,70 @@ def add_markdown_table(doc, rows, alignments):
968
1170
 
969
1171
 
970
1172
  def write_docx(path, title, content_markdown, product_name):
971
- """Write a simple, polished DOCX file locally."""
1173
+ """Write a professionally styled DOCX file locally."""
972
1174
  try:
973
1175
  from docx import Document
974
- from docx.shared import Inches, Pt
1176
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
1177
+ from docx.shared import Inches, Pt, RGBColor
975
1178
  except Exception as exc:
976
1179
  raise RuntimeError(
977
1180
  "python-docx is not installed. Re-run the official installer or install python-docx in the MCP runtime environment."
978
1181
  ) from exc
979
1182
 
980
1183
  doc = Document()
1184
+ doc.core_properties.title = title or f"{product_name} Work Product"
1185
+ doc.core_properties.author = product_name
1186
+
1187
+ section = doc.sections[0]
1188
+ section.top_margin = Inches(0.55)
1189
+ section.bottom_margin = Inches(0.55)
1190
+ section.left_margin = Inches(0.7)
1191
+ section.right_margin = Inches(0.7)
1192
+ section.header_distance = Inches(0.3)
1193
+ section.footer_distance = Inches(0.3)
1194
+
1195
+ styles = doc.styles
1196
+ normal = styles["Normal"]
1197
+ normal.font.name = "Aptos"
1198
+ normal.font.size = Pt(9.3)
1199
+ normal.font.color.rgb = RGBColor(31, 41, 55)
1200
+ normal.paragraph_format.space_after = Pt(4)
1201
+ normal.paragraph_format.line_spacing = 1.05
1202
+
1203
+ title_style = styles["Title"]
1204
+ title_style.font.name = "Aptos Display"
1205
+ title_style.font.size = Pt(19)
1206
+ title_style.font.bold = True
1207
+ title_style.font.color.rgb = RGBColor(23, 54, 93)
1208
+ title_style.paragraph_format.space_after = Pt(5)
1209
+
1210
+ heading_sizes = {1: 14, 2: 11.5, 3: 10.5, 4: 9.5}
1211
+ for level, size in heading_sizes.items():
1212
+ style = styles[f"Heading {level}"]
1213
+ style.font.name = "Aptos Display"
1214
+ style.font.size = Pt(size)
1215
+ style.font.bold = True
1216
+ style.font.color.rgb = RGBColor(23, 54, 93)
1217
+ style.paragraph_format.space_before = Pt(8 if level <= 2 else 5)
1218
+ style.paragraph_format.space_after = Pt(3)
1219
+ style.paragraph_format.keep_with_next = True
1220
+
1221
+ header = section.header.paragraphs[0]
1222
+ header.text = f"{product_name} | Professional Work Product"
1223
+ header.alignment = WD_ALIGN_PARAGRAPH.RIGHT
1224
+ for run in header.runs:
1225
+ run.font.name = "Aptos"
1226
+ run.font.size = Pt(7.5)
1227
+ run.font.color.rgb = RGBColor(100, 116, 139)
1228
+
1229
+ footer = section.footer.paragraphs[0]
1230
+ footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
1231
+ footer_run = footer.add_run(f"Generated locally by {product_name} | Private local output | Page ")
1232
+ footer_run.font.name = "Aptos"
1233
+ footer_run.font.size = Pt(7.5)
1234
+ footer_run.font.color.rgb = RGBColor(100, 116, 139)
1235
+ add_page_number(footer)
1236
+
981
1237
  if title:
982
1238
  doc.add_heading(title, level=0)
983
1239
 
@@ -987,16 +1243,24 @@ def write_docx(path, title, content_markdown, product_name):
987
1243
  def flush_paragraph():
988
1244
  if not paragraph_buffer:
989
1245
  return
990
- text = " ".join(part.strip() for part in paragraph_buffer if part.strip())
1246
+ text_parts = []
1247
+ for part in paragraph_buffer:
1248
+ clean = part.strip()
1249
+ if not clean:
1250
+ continue
1251
+ text_parts.append(clean)
1252
+ text_parts.append("\n" if part.endswith(" ") else " ")
1253
+ text = "".join(text_parts).rstrip()
991
1254
  paragraph_buffer.clear()
992
1255
  if text:
993
1256
  add_markdown_text(doc.add_paragraph(), text)
994
1257
 
995
1258
  in_code_block = False
1259
+ keep_checklist_together = False
996
1260
  index = 0
997
1261
  while index < len(lines):
998
1262
  raw_line = lines[index]
999
- line = raw_line.rstrip()
1263
+ line = raw_line.rstrip("\r\n")
1000
1264
  stripped = line.strip()
1001
1265
  if stripped.startswith("```"):
1002
1266
  flush_paragraph()
@@ -1016,9 +1280,19 @@ def write_docx(path, title, content_markdown, product_name):
1016
1280
  continue
1017
1281
 
1018
1282
  heading = re.match(r"^(#{1,4})\s+(.+)$", stripped)
1283
+ inferred_heading = inferred_plain_heading_level(lines, index)
1019
1284
  bullet = re.match(r"^(\s*)[-*+]\s+(.+)$", line)
1020
1285
  numbered = re.match(r"^(\s*)\d+\.\s+(.+)$", line)
1021
1286
  quote = re.match(r"^>\s?(.+)$", stripped)
1287
+ fact = (
1288
+ labeled_fact(stripped)
1289
+ if not paragraph_buffer
1290
+ and not line.endswith(" ")
1291
+ and not bullet
1292
+ and not numbered
1293
+ and not quote
1294
+ else None
1295
+ )
1022
1296
 
1023
1297
  if is_markdown_table_start(lines, index):
1024
1298
  flush_paragraph()
@@ -1036,7 +1310,24 @@ def write_docx(path, title, content_markdown, product_name):
1036
1310
  continue
1037
1311
  elif heading:
1038
1312
  flush_paragraph()
1039
- doc.add_heading(heading.group(2).strip(), level=min(len(heading.group(1)), 4))
1313
+ heading_text = heading.group(2).strip()
1314
+ if not (
1315
+ title
1316
+ and index == 0
1317
+ and re.sub(r"\W+", "", heading_text).casefold()
1318
+ == re.sub(r"\W+", "", title).casefold()
1319
+ ):
1320
+ doc.add_heading(heading_text, level=min(len(heading.group(1)), 4))
1321
+ keep_checklist_together = "checklist" in heading_text.casefold()
1322
+ index += 1
1323
+ elif inferred_heading:
1324
+ flush_paragraph()
1325
+ doc.add_heading(stripped, level=inferred_heading)
1326
+ keep_checklist_together = "checklist" in stripped.casefold()
1327
+ index += 1
1328
+ elif fact:
1329
+ flush_paragraph()
1330
+ add_labeled_fact(doc, fact[0], fact[1])
1040
1331
  index += 1
1041
1332
  elif bullet:
1042
1333
  flush_paragraph()
@@ -1047,7 +1338,19 @@ def write_docx(path, title, content_markdown, product_name):
1047
1338
  checkbox = re.match(r"^\[([ xX])\]\s+(.+)$", text)
1048
1339
  if checkbox:
1049
1340
  text = f"[{checkbox.group(1).lower()}] {checkbox.group(2).strip()}"
1050
- add_markdown_text(para, text)
1341
+ bullet_fact = labeled_fact(text)
1342
+ if bullet_fact:
1343
+ add_labeled_text(para, bullet_fact[0], bullet_fact[1])
1344
+ else:
1345
+ add_markdown_text(para, text)
1346
+ next_is_bullet = bool(
1347
+ index + 1 < len(lines)
1348
+ and re.match(r"^\s*[-*+]\s+\S", lines[index + 1])
1349
+ )
1350
+ if keep_checklist_together and next_is_bullet:
1351
+ para.paragraph_format.keep_with_next = True
1352
+ elif keep_checklist_together:
1353
+ keep_checklist_together = False
1051
1354
  index += 1
1052
1355
  elif numbered:
1053
1356
  flush_paragraph()
@@ -1070,17 +1373,11 @@ def write_docx(path, title, content_markdown, product_name):
1070
1373
  add_horizontal_rule(doc.add_paragraph())
1071
1374
  index += 1
1072
1375
  else:
1073
- paragraph_buffer.append(stripped)
1376
+ paragraph_buffer.append(line)
1074
1377
  index += 1
1075
1378
 
1076
1379
  flush_paragraph()
1077
1380
 
1078
- doc.add_paragraph()
1079
- footer = doc.add_paragraph()
1080
- run = footer.add_run(f"Generated locally by {product_name}. Content was not sent to TasksAI servers.")
1081
- run.italic = True
1082
- run.font.size = Pt(8)
1083
-
1084
1381
  doc.save(path)
1085
1382
 
1086
1383
 
@@ -1145,7 +1442,7 @@ def build_tools(prefix, product_name, occupation):
1145
1442
  domain_adjective = normalize_domain_adjective(occ_key)
1146
1443
  examples = _examples.get(domain_adjective, _examples.get(product_name.replace('TasksAI','').lower().strip(), f"e.g. a {domain_adjective} task"))
1147
1444
 
1148
- return [
1445
+ tools = [
1149
1446
  Tool(
1150
1447
  name=f"{prefix}_search",
1151
1448
  description=(
@@ -1250,6 +1547,46 @@ def build_tools(prefix, product_name, occupation):
1250
1547
  ),
1251
1548
  ]
1252
1549
 
1550
+ if prefix in {"realtortasksai", "farmertasksai", "teachertasksai"}:
1551
+ tools.insert(
1552
+ 2,
1553
+ Tool(
1554
+ name=f"{prefix}_jurisdiction_sources",
1555
+ description=(
1556
+ f"Retrieve the curated current jurisdiction-and-authority source pack for a jurisdiction-sensitive {product_name} workflow. "
1557
+ "Use after the applicable state is known and before applying state, local, district, regulator, form, or policy requirements. "
1558
+ "Send only the skill ID and general authority labels; never send an address, person name, student or client facts, documents, or generated content."
1559
+ ),
1560
+ inputSchema={
1561
+ "type": "object",
1562
+ "properties": {
1563
+ "skill_id": {
1564
+ "type": "string",
1565
+ "description": "Exact workflow ID requesting a task-specific source pack.",
1566
+ },
1567
+ "state": {
1568
+ "type": "string",
1569
+ "description": "Property state name or two-letter code. Omit only to obtain a blocking missing-jurisdiction response.",
1570
+ },
1571
+ "county": {
1572
+ "type": "string",
1573
+ "description": "Optional county name only; do not include an address.",
1574
+ },
1575
+ "locality": {
1576
+ "type": "string",
1577
+ "description": "Optional city or municipality name only; do not include an address.",
1578
+ },
1579
+ "district": {
1580
+ "type": "string",
1581
+ "description": "Optional public authority or school-district name only; do not include a person, student, or record identifier.",
1582
+ },
1583
+ },
1584
+ "required": ["skill_id"],
1585
+ },
1586
+ ),
1587
+ )
1588
+ return tools
1589
+
1253
1590
 
1254
1591
  def normalize_audience_label(label):
1255
1592
  """Return a natural audience phrase without naive pluralization."""
@@ -1441,6 +1778,16 @@ async def call_tool(name, arguments):
1441
1778
  f"---\n*Credits remaining: {credits_remaining}*"
1442
1779
  ))]
1443
1780
 
1781
+ # ── Jurisdiction Sources ─────────────────────────────────────────────
1782
+ elif name == f"{prefix}_jurisdiction_sources" and prefix in {
1783
+ "realtortasksai",
1784
+ "farmertasksai",
1785
+ "teachertasksai",
1786
+ }:
1787
+ path = jurisdiction_sources_path(arguments or {})
1788
+ result = await api_get(path)
1789
+ return [TextContent(type="text", text=format_jurisdiction_source_pack(result))]
1790
+
1444
1791
  # ── Save Document ───────────────────────────────────────────────────
1445
1792
  elif name == f"{prefix}_save_document":
1446
1793
  output_path, fmt = save_document(arguments or {}, product_name)
@@ -57,6 +57,12 @@ DEFAULT_CONTEXT_WORDS = {
57
57
  DEFAULT_THRESHOLDS = {"high": 90, "medium": 45}
58
58
  DEFAULT_RESULT_COUNT = 3
59
59
  MAXIMUM_RESULT_COUNT = 10
60
+ TRIGGER_EVIDENCE_RANK = {
61
+ "trigger_exact": 4,
62
+ "trigger_complete": 3,
63
+ "trigger_query_complete": 2,
64
+ "trigger_partial": 1,
65
+ }
60
66
 
61
67
 
62
68
  def normalize_text(value: object) -> str:
@@ -386,6 +392,7 @@ def _score_skill(
386
392
  "score": score,
387
393
  "confidence": confidence,
388
394
  "evidence": evidence,
395
+ "_trigger_rank": TRIGGER_EVIDENCE_RANK.get(trigger_evidence, 0),
389
396
  "_trigger_specificity": trigger_specificity,
390
397
  "_matched_tokens": matched_tokens,
391
398
  "_description_normalized": normalize_text(skill.get("description", "")),
@@ -395,6 +402,7 @@ def _score_skill(
395
402
 
396
403
  def _sort_key(result: Mapping[str, object]) -> tuple:
397
404
  return (
405
+ -result["_trigger_rank"],
398
406
  -result["score"],
399
407
  -result["_trigger_specificity"],
400
408
  -len(result["_matched_tokens"]),
package/src/index.js CHANGED
@@ -47,32 +47,40 @@ const LEGACY_MCP_SERVER_IDS = {
47
47
  teacher: ["teachertasksai"]
48
48
  };
49
49
 
50
+ // Test/development override used to keep synthetic installs away from a user's
51
+ // active client profile. It is intentionally environment-only and is not part
52
+ // of the normal customer installation flow.
53
+ function isolatedClientConfigPath(fileName, defaultPath) {
54
+ const root = process.env.TASKSAI_CLIENT_CONFIG_DIR;
55
+ return root ? path.join(path.resolve(root), fileName) : defaultPath;
56
+ }
57
+
50
58
  const CLIENTS = {
51
59
  "claude-desktop": {
52
60
  id: "claude-desktop",
53
61
  displayName: "Claude Desktop",
54
62
  configPath() {
55
63
  if (process.platform === "darwin") {
56
- return path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
64
+ return isolatedClientConfigPath("claude_desktop_config.json", path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json"));
57
65
  }
58
66
  if (process.platform === "win32") {
59
- return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
67
+ return isolatedClientConfigPath("claude_desktop_config.json", path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json"));
60
68
  }
61
- return path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json");
69
+ return isolatedClientConfigPath("claude_desktop_config.json", path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json"));
62
70
  }
63
71
  },
64
72
  cursor: {
65
73
  id: "cursor",
66
74
  displayName: "Cursor",
67
75
  configPath() {
68
- return path.join(os.homedir(), ".cursor", "mcp.json");
76
+ return isolatedClientConfigPath("mcp.json", path.join(os.homedir(), ".cursor", "mcp.json"));
69
77
  }
70
78
  },
71
79
  windsurf: {
72
80
  id: "windsurf",
73
81
  displayName: "Windsurf",
74
82
  configPath() {
75
- return path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json");
83
+ return isolatedClientConfigPath("mcp_config.json", path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json"));
76
84
  }
77
85
  },
78
86
  codex: {
@@ -80,7 +88,7 @@ const CLIENTS = {
80
88
  displayName: "Codex",
81
89
  configFormat: "toml",
82
90
  configPath() {
83
- return path.join(os.homedir(), ".codex", "config.toml");
91
+ return isolatedClientConfigPath("config.toml", path.join(os.homedir(), ".codex", "config.toml"));
84
92
  }
85
93
  }
86
94
  };
@@ -196,6 +204,7 @@ Examples:
196
204
 
197
205
  Environment:
198
206
  TASKSAI_INSTALL_DIR may be used instead of --install-dir.
207
+ TASKSAI_CLIENT_CONFIG_DIR may be used for an isolated test/development client profile.
199
208
 
200
209
  Development only:
201
210
  --allow-untrusted-development-source permits a local, non-main, or non-official