@tasksai/install 0.1.38 → 0.1.40

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 (74) hide show
  1. package/README.md +34 -0
  2. package/bootstrap/Install RealtorTasksAI.command +37 -0
  3. package/bootstrap/Install RealtorTasksAI.ps1 +42 -0
  4. package/package.json +14 -3
  5. package/runtime/document_renderer.py +882 -0
  6. package/runtime/server.py +55 -581
  7. package/runtime/software_use.py +39 -0
  8. package/runtime/workflow-requirements.txt +5 -0
  9. package/runtime/workflows/__init__.py +0 -0
  10. package/runtime/workflows/account_snapshot.py +33 -0
  11. package/runtime/workflows/attachments.py +45 -0
  12. package/runtime/workflows/authority_guides.py +81 -0
  13. package/runtime/workflows/catalog.py +51 -0
  14. package/runtime/workflows/catalog_app.py +174 -0
  15. package/runtime/workflows/catalog_generation.py +186 -0
  16. package/runtime/workflows/catalog_output.py +312 -0
  17. package/runtime/workflows/catalog_suggestions.py +51 -0
  18. package/runtime/workflows/catalog_workspace.py +152 -0
  19. package/runtime/workflows/cli.py +66 -0
  20. package/runtime/workflows/document_answers.py +96 -0
  21. package/runtime/workflows/document_selection.py +50 -0
  22. package/runtime/workflows/documents.py +243 -0
  23. package/runtime/workflows/embedded/catalog.html +83 -0
  24. package/runtime/workflows/embedded/offer-review.html +516 -0
  25. package/runtime/workflows/embedded/preview.html +36 -0
  26. package/runtime/workflows/embedded_actions.py +96 -0
  27. package/runtime/workflows/embedded_demo.py +424 -0
  28. package/runtime/workflows/folders.py +120 -0
  29. package/runtime/workflows/gateway.py +157 -0
  30. package/runtime/workflows/generation_lock.py +26 -0
  31. package/runtime/workflows/launcher.py +61 -0
  32. package/runtime/workflows/licensed_delivery.py +46 -0
  33. package/runtime/workflows/mcp_dev.py +52 -0
  34. package/runtime/workflows/meeting_plan.py +92 -0
  35. package/runtime/workflows/model_client.py +26 -0
  36. package/runtime/workflows/numeric_consistency.py +72 -0
  37. package/runtime/workflows/public_source_fetch.py +66 -0
  38. package/runtime/workflows/realtor/__init__.py +0 -0
  39. package/runtime/workflows/realtor/adapter.py +179 -0
  40. package/runtime/workflows/realtor/seller_offer/ORIGIN.json +16 -0
  41. package/runtime/workflows/realtor/seller_offer/__init__.py +0 -0
  42. package/runtime/workflows/realtor/seller_offer/examples/demo-input.json +414 -0
  43. package/runtime/workflows/realtor/seller_offer/examples/make_demo.py +44 -0
  44. package/runtime/workflows/realtor/seller_offer/references/input-contract.md +65 -0
  45. package/runtime/workflows/realtor/seller_offer/references/source-boundaries.md +16 -0
  46. package/runtime/workflows/realtor/seller_offer/scripts/__init__.py +0 -0
  47. package/runtime/workflows/realtor/seller_offer/scripts/build_workbook.mjs +233 -0
  48. package/runtime/workflows/realtor/seller_offer/scripts/build_workbook.py +163 -0
  49. package/runtime/workflows/realtor/seller_offer/scripts/offer_engine.py +370 -0
  50. package/runtime/workflows/realtor/seller_offer/scripts/presentation.py +52 -0
  51. package/runtime/workflows/realtor/seller_offer/scripts/render_outputs.py +228 -0
  52. package/runtime/workflows/realtor/seller_offer/scripts/run_package.py +95 -0
  53. package/runtime/workflows/realtor/seller_offer/tests/test_engine.py +252 -0
  54. package/runtime/workflows/realtor/seller_offer/tests/test_workbook.mjs +28 -0
  55. package/runtime/workflows/realtor-release-registry.json +705 -0
  56. package/runtime/workflows/released_catalog.py +91 -0
  57. package/runtime/workflows/source_capture.py +82 -0
  58. package/runtime/workflows/store.py +492 -0
  59. package/runtime/workflows/table_calculations.py +132 -0
  60. package/runtime/workflows/template.py +65 -0
  61. package/runtime/workflows/workspace_launch.py +76 -0
  62. package/src/index.js +217 -118
  63. package/src/managed-python.js +63 -0
  64. package/src/operation-lock.js +22 -0
  65. package/src/prepared-update.js +86 -0
  66. package/src/private-workflow.js +116 -0
  67. package/src/python-runtime.js +50 -0
  68. package/src/recover-installation.js +30 -0
  69. package/src/recovery-lock.js +30 -0
  70. package/src/software-hash.js +24 -0
  71. package/src/software-use.js +32 -0
  72. package/src/update-journal.js +24 -0
  73. package/src/update-recovery.js +72 -0
  74. package/src/workspace-runtime.js +45 -0
package/runtime/server.py CHANGED
@@ -7,15 +7,16 @@ then self-configures tool names, system prompt, and abbreviation maps.
7
7
 
8
8
  Tools (names are vertical-prefixed at runtime, e.g. farmertasksai_search):
9
9
  {prefix}_search — Find the right skill for your task
10
- {prefix}_execute — Get the full expert framework for a skill (costs 1 credit)
10
+ {prefix}_execute — Get the full expert framework at its configured credit charge
11
11
  {prefix}_save_document — Save the finished local output as a downloadable file
12
12
  {prefix}_balance — Check your remaining credit balance
13
13
  {prefix}_categories — Browse skills by category
14
14
 
15
- Privacy: Your queries, documents, and client data never leave your machine.
16
- Skills run entirely locally. The API only delivers skill metadata and
17
- counts credits it never sees what you're working on. Saved documents are
18
- created by this local MCP server, not by TasksAI websites or APIs.
15
+ Privacy: TasksAI saves project data and document files locally. Its APIs deliver
16
+ skill content, account/credit information and authority-source metadata; they do
17
+ not receive customer document content through this workflow. Cloud AI processing
18
+ is separate: the selected AI provider receives the information supplied to it.
19
+ The local workspace asks for consent before sending selected information to AI.
19
20
  """
20
21
 
21
22
  import os
@@ -891,580 +892,9 @@ def unique_output_path(root, filename, overwrite=False):
891
892
  raise RuntimeError("Could not choose a unique output filename.")
892
893
 
893
894
 
894
- def markdown_table_row(line):
895
- """Return parsed table cells when a line looks like a Markdown table row."""
896
- stripped = line.strip()
897
- if "|" not in stripped:
898
- return None
899
- if stripped.startswith("|"):
900
- stripped = stripped[1:]
901
- if stripped.endswith("|"):
902
- stripped = stripped[:-1]
903
- cells = []
904
- current = []
905
- escaped = False
906
- for char in stripped:
907
- if escaped:
908
- current.append(char)
909
- escaped = False
910
- elif char == "\\":
911
- escaped = True
912
- elif char == "|":
913
- cells.append("".join(current).strip())
914
- current = []
915
- else:
916
- current.append(char)
917
- cells.append("".join(current).strip())
918
- return cells if len(cells) > 1 else None
919
-
920
-
921
- def markdown_table_alignments(separator_cells):
922
- """Return column alignments if cells are a valid Markdown table separator."""
923
- alignments = []
924
- for cell in separator_cells or []:
925
- compact = cell.replace(" ", "")
926
- if not re.match(r"^:?-{3,}:?$", compact):
927
- return None
928
- if compact.startswith(":") and compact.endswith(":"):
929
- alignments.append("center")
930
- elif compact.endswith(":"):
931
- alignments.append("right")
932
- else:
933
- alignments.append("left")
934
- return alignments
935
-
936
-
937
- def is_markdown_table_start(lines, index):
938
- """True when lines[index:index+2] form a Markdown table header."""
939
- if index + 1 >= len(lines):
940
- return False
941
- header = markdown_table_row(lines[index])
942
- separator = markdown_table_row(lines[index + 1])
943
- return bool(header and markdown_table_alignments(separator))
944
-
945
-
946
- def inferred_plain_heading_level(lines, index):
947
- """Infer conservative headings when an AI omits Markdown # markers."""
948
- text = lines[index].strip()
949
- if not text or len(text) > 90 or "|" in text or ":" in text:
950
- return None
951
- if re.match(r"^(?:[-*+]\s+|\d+\.\s+|>|<!--)", text):
952
- return None
953
- if text.endswith((".", "?", "!", ";")):
954
- return None
955
-
956
- letters = [char for char in text if char.isalpha()]
957
- if len(letters) >= 4 and all(char.isupper() for char in letters):
958
- return 2
959
-
960
- next_index = index + 1
961
- while next_index < len(lines) and not lines[next_index].strip():
962
- next_index += 1
963
- next_line = lines[next_index] if next_index < len(lines) else ""
964
- introduces_list = bool(
965
- re.match(r"^\s*[-*+]\s+\S", next_line)
966
- or re.match(r"^\s*\d+\.\s+\S", next_line)
967
- )
968
- introduces_facts = bool(re.match(r"^\s*[^:\n]{1,55}:\s+\S", next_line))
969
- if introduces_list or introduces_facts or text.istitle():
970
- return 3
971
- return None
972
-
973
-
974
- def labeled_fact(line):
975
- """Return a label/value pair for a short professional fact line."""
976
- bold_match = re.match(r"^\*\*([^*\n:]{1,55}):\*\*\s+(.+)$", line.strip())
977
- if bold_match:
978
- return bold_match.group(1).strip(), bold_match.group(2).strip()
979
- match = re.match(r"^([^:\n]{1,55}):\s+(.+)$", line.strip())
980
- if not match:
981
- return None
982
- if re.search(r"[`*_\[\]]", match.group(1)):
983
- # A colon after inline Markdown is sentence punctuation, not a
984
- # short label/value field. Let the normal inline parser handle it.
985
- return None
986
- return match.group(1).strip(), match.group(2).strip()
987
-
988
-
989
- def add_labeled_text(paragraph, label, value):
990
- """Render a label/value pair in an existing Word paragraph."""
991
- label_run = paragraph.add_run(f"{label}: ")
992
- label_run.bold = True
993
- add_markdown_text(paragraph, value)
994
-
995
-
996
- def add_labeled_fact(doc, label, value):
997
- """Render a fact line with a bold label and distinct Word paragraph."""
998
- paragraph = doc.add_paragraph()
999
- add_labeled_text(paragraph, label, value)
1000
-
1001
-
1002
- def iter_inline_markdown(text):
1003
- """Yield (text, marks) spans for common inline Markdown."""
1004
- text = re.sub(r"\\([\\`*_{}\[\]()#+.!-])", r"\1", text)
1005
- token_re = re.compile(
1006
- r"(`[^`]+`|\*\*[^*]+\*\*|(?<!_)__(?![_\s])[^_]+?(?<!\s)__(?!_)|\*[^*\s][^*]*\*|(?<!\w)_[^_\s][^_]*_(?!\w)|\[[^\]]+\]\([^)]+\))"
1007
- )
1008
- pos = 0
1009
- for match in token_re.finditer(text):
1010
- if match.start() > pos:
1011
- yield text[pos:match.start()], {}
1012
- token = match.group(0)
1013
- marks = {}
1014
- value = token
1015
- if token.startswith("**") and token.endswith("**"):
1016
- value = token[2:-2]
1017
- marks["bold"] = True
1018
- elif token.startswith("__") and token.endswith("__"):
1019
- value = token[2:-2]
1020
- marks["bold"] = True
1021
- elif token.startswith("`") and token.endswith("`"):
1022
- value = token[1:-1]
1023
- marks["code"] = True
1024
- elif token.startswith("["):
1025
- link = re.match(r"^\[([^\]]+)\]\(([^)]+)\)$", token)
1026
- if link:
1027
- value = link.group(1)
1028
- marks["link"] = link.group(2)
1029
- elif token[0] in {"*", "_"} and token.endswith(token[0]):
1030
- value = token[1:-1]
1031
- marks["italic"] = True
1032
- yield value, marks
1033
- pos = match.end()
1034
- if pos < len(text):
1035
- yield text[pos:], {}
1036
-
1037
-
1038
- def add_hyperlink(paragraph, text, url):
1039
- """Add a clickable hyperlink run when python-docx low-level APIs are available."""
1040
- from docx.oxml import OxmlElement
1041
- from docx.oxml.ns import qn
1042
-
1043
- part = paragraph.part
1044
- relationship_id = part.relate_to(
1045
- url,
1046
- "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
1047
- is_external=True,
1048
- )
1049
-
1050
- hyperlink = OxmlElement("w:hyperlink")
1051
- hyperlink.set(qn("r:id"), relationship_id)
1052
- run_element = OxmlElement("w:r")
1053
- properties = OxmlElement("w:rPr")
1054
-
1055
- color = OxmlElement("w:color")
1056
- color.set(qn("w:val"), "0563C1")
1057
- properties.append(color)
1058
-
1059
- underline = OxmlElement("w:u")
1060
- underline.set(qn("w:val"), "single")
1061
- properties.append(underline)
1062
-
1063
- run_element.append(properties)
1064
- text_element = OxmlElement("w:t")
1065
- text_element.text = text
1066
- run_element.append(text_element)
1067
- hyperlink.append(run_element)
1068
- paragraph._p.append(hyperlink)
1069
-
1070
-
1071
- def add_markdown_text(paragraph, text):
1072
- """Add common inline Markdown formatting to a python-docx paragraph."""
1073
- for value, marks in iter_inline_markdown(text):
1074
- if not value:
1075
- continue
1076
- if marks.get("link"):
1077
- try:
1078
- add_hyperlink(paragraph, value, marks["link"])
1079
- except Exception:
1080
- paragraph.add_run(f"{value} ({marks['link']})")
1081
- continue
1082
- run = paragraph.add_run(value)
1083
- if marks.get("bold"):
1084
- run.bold = True
1085
- if re.match(r"^\+\d", value):
1086
- # Aptos substitution in some headless Word renderers maps a
1087
- # bold leading plus sign to a diamond. Arial preserves the
1088
- # intended glyph and remains portable in Office.
1089
- run.font.name = "Arial"
1090
- if marks.get("italic"):
1091
- run.italic = True
1092
- if marks.get("code"):
1093
- run.font.name = "Courier New"
1094
-
1095
-
1096
- def add_markdown_heading(doc, text, level):
1097
- """Create a Word heading while parsing any inline Markdown in its text."""
1098
-
1099
- paragraph = doc.add_heading(level=level)
1100
- add_markdown_text(paragraph, text)
1101
- return paragraph
1102
-
1103
-
1104
- def add_horizontal_rule(paragraph):
1105
- """Render a horizontal Markdown rule as a Word paragraph border."""
1106
- from docx.oxml import OxmlElement
1107
- from docx.oxml.ns import qn
1108
-
1109
- p_pr = paragraph._p.get_or_add_pPr()
1110
- borders = p_pr.first_child_found_in("w:pBdr")
1111
- if borders is None:
1112
- borders = OxmlElement("w:pBdr")
1113
- p_pr.append(borders)
1114
- bottom = OxmlElement("w:bottom")
1115
- bottom.set(qn("w:val"), "single")
1116
- bottom.set(qn("w:sz"), "6")
1117
- bottom.set(qn("w:space"), "1")
1118
- bottom.set(qn("w:color"), "BFBFBF")
1119
- borders.append(bottom)
1120
-
1121
-
1122
- def set_cell_shading(cell, fill):
1123
- """Apply a solid background color to a Word table cell."""
1124
- from docx.oxml import OxmlElement
1125
- from docx.oxml.ns import qn
1126
-
1127
- cell_properties = cell._tc.get_or_add_tcPr()
1128
- shading = cell_properties.find(qn("w:shd"))
1129
- if shading is None:
1130
- shading = OxmlElement("w:shd")
1131
- cell_properties.append(shading)
1132
- shading.set(qn("w:fill"), fill)
1133
-
1134
-
1135
- def set_cell_margins(cell, top=70, start=90, bottom=70, end=90):
1136
- """Set compact, readable table cell margins in twentieths of a point."""
1137
- from docx.oxml import OxmlElement
1138
- from docx.oxml.ns import qn
1139
-
1140
- cell_properties = cell._tc.get_or_add_tcPr()
1141
- margins = cell_properties.first_child_found_in("w:tcMar")
1142
- if margins is None:
1143
- margins = OxmlElement("w:tcMar")
1144
- cell_properties.append(margins)
1145
- for name, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
1146
- node = margins.find(qn(f"w:{name}"))
1147
- if node is None:
1148
- node = OxmlElement(f"w:{name}")
1149
- margins.append(node)
1150
- node.set(qn("w:w"), str(value))
1151
- node.set(qn("w:type"), "dxa")
1152
-
1153
-
1154
- def repeat_table_header(row):
1155
- """Repeat the first row of a long table on subsequent pages."""
1156
- from docx.oxml import OxmlElement
1157
- from docx.oxml.ns import qn
1158
-
1159
- row_properties = row._tr.get_or_add_trPr()
1160
- repeat = row_properties.find(qn("w:tblHeader"))
1161
- if repeat is None:
1162
- repeat = OxmlElement("w:tblHeader")
1163
- row_properties.append(repeat)
1164
- repeat.set(qn("w:val"), "true")
1165
-
1166
-
1167
- def prevent_table_row_split(row):
1168
- """Keep a table row together when Word paginates the document."""
1169
- from docx.oxml import OxmlElement
1170
- from docx.oxml.ns import qn
1171
-
1172
- row_properties = row._tr.get_or_add_trPr()
1173
- if row_properties.find(qn("w:cantSplit")) is None:
1174
- row_properties.append(OxmlElement("w:cantSplit"))
1175
-
1176
-
1177
- def add_page_number(paragraph):
1178
- """Add a dynamic PAGE field to a footer paragraph."""
1179
- from docx.oxml import OxmlElement
1180
- from docx.oxml.ns import qn
1181
-
1182
- run = paragraph.add_run()
1183
- begin = OxmlElement("w:fldChar")
1184
- begin.set(qn("w:fldCharType"), "begin")
1185
- instruction = OxmlElement("w:instrText")
1186
- instruction.set(qn("xml:space"), "preserve")
1187
- instruction.text = " PAGE "
1188
- end = OxmlElement("w:fldChar")
1189
- end.set(qn("w:fldCharType"), "end")
1190
- run._r.extend((begin, instruction, end))
1191
-
1192
-
1193
- def add_markdown_table(doc, rows, alignments):
1194
- """Render a Markdown pipe table as a native Word table."""
1195
- from docx.enum.text import WD_ALIGN_PARAGRAPH
1196
- from docx.shared import Pt, RGBColor
1197
-
1198
- if not rows:
1199
- return
1200
-
1201
- width = max(len(row) for row in rows)
1202
- normalized = [row + [""] * (width - len(row)) for row in rows]
1203
- table = doc.add_table(rows=len(normalized), cols=width)
1204
- table.style = "Table Grid"
1205
- table.autofit = True
1206
- repeat_table_header(table.rows[0])
1207
- for row_index, row in enumerate(normalized):
1208
- row_characters = sum(len(value) for value in row)
1209
- longest_cell = max((len(value) for value in row), default=0)
1210
- if row_index == 0 or (row_characters <= 600 and longest_cell <= 350):
1211
- # Keep ordinary rows intact so a cell does not strand a fragment
1212
- # on the next page. Exceptionally large rows may still split to
1213
- # avoid creating a mostly blank page.
1214
- prevent_table_row_split(table.rows[row_index])
1215
- for column_index, value in enumerate(row):
1216
- cell = table.cell(row_index, column_index)
1217
- set_cell_margins(cell)
1218
- if row_index == 0:
1219
- set_cell_shading(cell, "17365D")
1220
- elif row_index % 2 == 0:
1221
- set_cell_shading(cell, "EEF3F8")
1222
- paragraph = cell.paragraphs[0]
1223
- paragraph.paragraph_format.space_after = Pt(0)
1224
- add_markdown_text(paragraph, value)
1225
- if row_index == 0:
1226
- for run in paragraph.runs:
1227
- run.bold = True
1228
- run.font.color.rgb = RGBColor(255, 255, 255)
1229
- alignment = alignments[column_index] if column_index < len(alignments) else "left"
1230
- if alignment == "right":
1231
- paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
1232
- elif alignment == "center":
1233
- paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
1234
- for run in paragraph.runs:
1235
- run.font.size = Pt(8 if width > 6 else 9)
1236
-
1237
- doc.add_paragraph()
1238
-
1239
-
1240
- def write_docx(path, title, content_markdown, product_name):
1241
- """Write a professionally styled DOCX file locally."""
1242
- try:
1243
- from docx import Document
1244
- from docx.enum.text import WD_ALIGN_PARAGRAPH
1245
- from docx.shared import Inches, Pt, RGBColor
1246
- except Exception as exc:
1247
- raise RuntimeError(
1248
- "python-docx is not installed. Re-run the official installer or install python-docx in the MCP runtime environment."
1249
- ) from exc
1250
-
1251
- doc = Document()
1252
- doc.core_properties.title = title or f"{product_name} Work Product"
1253
- doc.core_properties.author = product_name
1254
-
1255
- section = doc.sections[0]
1256
- section.top_margin = Inches(0.55)
1257
- section.bottom_margin = Inches(0.55)
1258
- section.left_margin = Inches(0.7)
1259
- section.right_margin = Inches(0.7)
1260
- section.header_distance = Inches(0.3)
1261
- section.footer_distance = Inches(0.3)
1262
-
1263
- styles = doc.styles
1264
- normal = styles["Normal"]
1265
- normal.font.name = "Aptos"
1266
- normal.font.size = Pt(9.3)
1267
- normal.font.color.rgb = RGBColor(31, 41, 55)
1268
- normal.paragraph_format.space_after = Pt(4)
1269
- normal.paragraph_format.line_spacing = 1.05
1270
-
1271
- title_style = styles["Title"]
1272
- title_style.font.name = "Aptos Display"
1273
- title_style.font.size = Pt(19)
1274
- title_style.font.bold = True
1275
- title_style.font.color.rgb = RGBColor(23, 54, 93)
1276
- title_style.paragraph_format.space_after = Pt(5)
1277
-
1278
- heading_sizes = {1: 14, 2: 11.5, 3: 10.5, 4: 9.5}
1279
- for level, size in heading_sizes.items():
1280
- style = styles[f"Heading {level}"]
1281
- style.font.name = "Aptos Display"
1282
- style.font.size = Pt(size)
1283
- style.font.bold = True
1284
- style.font.color.rgb = RGBColor(23, 54, 93)
1285
- style.paragraph_format.space_before = Pt(8 if level <= 2 else 5)
1286
- style.paragraph_format.space_after = Pt(3)
1287
- style.paragraph_format.keep_with_next = True
1288
-
1289
- header = section.header.paragraphs[0]
1290
- header.text = f"{product_name} | Professional Work Product"
1291
- header.alignment = WD_ALIGN_PARAGRAPH.RIGHT
1292
- for run in header.runs:
1293
- run.font.name = "Aptos"
1294
- run.font.size = Pt(7.5)
1295
- run.font.color.rgb = RGBColor(100, 116, 139)
1296
-
1297
- footer = section.footer.paragraphs[0]
1298
- footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
1299
- footer_run = footer.add_run(f"Generated locally by {product_name} | Private local output | Page ")
1300
- footer_run.font.name = "Aptos"
1301
- footer_run.font.size = Pt(7.5)
1302
- footer_run.font.color.rgb = RGBColor(100, 116, 139)
1303
- add_page_number(footer)
1304
-
1305
- if title:
1306
- doc.add_heading(title, level=0)
1307
-
1308
- lines = content_markdown.splitlines()
1309
- paragraph_buffer = []
1310
-
1311
- def flush_paragraph():
1312
- if not paragraph_buffer:
1313
- return
1314
- text_parts = []
1315
- for part in paragraph_buffer:
1316
- clean = part.strip()
1317
- if not clean:
1318
- continue
1319
- text_parts.append(clean)
1320
- text_parts.append("\n" if part.endswith(" ") else " ")
1321
- text = "".join(text_parts).rstrip()
1322
- paragraph_buffer.clear()
1323
- if text:
1324
- add_markdown_text(doc.add_paragraph(), text)
1325
-
1326
- in_code_block = False
1327
- keep_checklist_together = False
1328
- index = 0
1329
- while index < len(lines):
1330
- raw_line = lines[index]
1331
- line = raw_line.rstrip("\r\n")
1332
- stripped = line.strip()
1333
- if stripped.startswith("```"):
1334
- flush_paragraph()
1335
- in_code_block = not in_code_block
1336
- index += 1
1337
- continue
1338
- if in_code_block:
1339
- para = doc.add_paragraph()
1340
- run = para.add_run(line)
1341
- run.font.name = "Courier New"
1342
- run.font.size = Pt(9)
1343
- index += 1
1344
- continue
1345
- if not stripped:
1346
- flush_paragraph()
1347
- index += 1
1348
- continue
1349
-
1350
- heading = re.match(r"^(#{1,4})\s+(.+)$", stripped)
1351
- standalone_bold_heading = re.fullmatch(r"\*\*([^*\n]{1,120})\*\*", stripped)
1352
- inferred_heading = inferred_plain_heading_level(lines, index)
1353
- bullet = re.match(r"^(\s*)[-*+]\s+(.+)$", line)
1354
- numbered = re.match(r"^(\s*)\d+\.\s+(.+)$", line)
1355
- quote = re.match(r"^>\s?(.+)$", stripped)
1356
- fact = (
1357
- labeled_fact(stripped)
1358
- if not paragraph_buffer
1359
- and not line.endswith(" ")
1360
- and not bullet
1361
- and not numbered
1362
- and not quote
1363
- else None
1364
- )
1365
-
1366
- if re.fullmatch(r"<!--\s*[^\n]*?\s*-->", stripped):
1367
- # Public-copy delimiters and other machine-readable Markdown
1368
- # comments are useful in the source but must not appear in the
1369
- # downloadable customer document.
1370
- flush_paragraph()
1371
- index += 1
1372
- continue
1373
- if stripped == ">":
1374
- # A blank line inside a Markdown blockquote is structural spacing,
1375
- # not a literal greater-than character in the Word document.
1376
- flush_paragraph()
1377
- index += 1
1378
- continue
1379
- if is_markdown_table_start(lines, index):
1380
- flush_paragraph()
1381
- header = markdown_table_row(lines[index])
1382
- alignments = markdown_table_alignments(markdown_table_row(lines[index + 1]))
1383
- table_rows = [header]
1384
- index += 2
1385
- while index < len(lines):
1386
- row = markdown_table_row(lines[index])
1387
- if not row:
1388
- break
1389
- table_rows.append(row)
1390
- index += 1
1391
- add_markdown_table(doc, table_rows, alignments)
1392
- continue
1393
- elif heading:
1394
- flush_paragraph()
1395
- heading_text = heading.group(2).strip()
1396
- if not (
1397
- title
1398
- and index == 0
1399
- and re.sub(r"\W+", "", heading_text).casefold()
1400
- == re.sub(r"\W+", "", title).casefold()
1401
- ):
1402
- add_markdown_heading(doc, heading_text, min(len(heading.group(1)), 4))
1403
- keep_checklist_together = "checklist" in heading_text.casefold()
1404
- index += 1
1405
- elif standalone_bold_heading:
1406
- flush_paragraph()
1407
- add_markdown_heading(doc, standalone_bold_heading.group(1).strip(), 3)
1408
- index += 1
1409
- elif inferred_heading:
1410
- flush_paragraph()
1411
- add_markdown_heading(doc, stripped, inferred_heading)
1412
- keep_checklist_together = "checklist" in stripped.casefold()
1413
- index += 1
1414
- elif fact:
1415
- flush_paragraph()
1416
- add_labeled_fact(doc, fact[0], fact[1])
1417
- index += 1
1418
- elif bullet:
1419
- flush_paragraph()
1420
- para = doc.add_paragraph(style="List Bullet")
1421
- indent_level = min(len(bullet.group(1).replace("\t", " ")) // 2, 4)
1422
- para.paragraph_format.left_indent = Inches(0.25 * indent_level)
1423
- text = bullet.group(2).strip()
1424
- checkbox = re.match(r"^\[([ xX])\]\s+(.+)$", text)
1425
- if checkbox:
1426
- text = f"[{checkbox.group(1).lower()}] {checkbox.group(2).strip()}"
1427
- bullet_fact = labeled_fact(text)
1428
- if bullet_fact:
1429
- add_labeled_text(para, bullet_fact[0], bullet_fact[1])
1430
- else:
1431
- add_markdown_text(para, text)
1432
- next_is_bullet = bool(
1433
- index + 1 < len(lines)
1434
- and re.match(r"^\s*[-*+]\s+\S", lines[index + 1])
1435
- )
1436
- if keep_checklist_together and next_is_bullet:
1437
- para.paragraph_format.keep_with_next = True
1438
- elif keep_checklist_together:
1439
- keep_checklist_together = False
1440
- index += 1
1441
- elif numbered:
1442
- flush_paragraph()
1443
- para = doc.add_paragraph(style="List Number")
1444
- indent_level = min(len(numbered.group(1).replace("\t", " ")) // 2, 4)
1445
- para.paragraph_format.left_indent = Inches(0.25 * indent_level)
1446
- add_markdown_text(para, numbered.group(2).strip())
1447
- index += 1
1448
- elif quote:
1449
- flush_paragraph()
1450
- try:
1451
- para = doc.add_paragraph(style="Quote")
1452
- except Exception:
1453
- para = doc.add_paragraph()
1454
- para.paragraph_format.left_indent = Inches(0.25)
1455
- add_markdown_text(para, quote.group(1).strip())
1456
- index += 1
1457
- elif stripped in {"---", "***", "___"}:
1458
- flush_paragraph()
1459
- add_horizontal_rule(doc.add_paragraph())
1460
- index += 1
1461
- else:
1462
- paragraph_buffer.append(line)
1463
- index += 1
1464
-
1465
- flush_paragraph()
1466
-
1467
- doc.save(path)
895
+ # Also works when the server is loaded by an embedding host via a file spec.
896
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
897
+ from document_renderer import * # shared renderer; preserves the existing helper API
1468
898
 
1469
899
 
1470
900
  def write_markdown(path, title, content_markdown, product_name):
@@ -1749,6 +1179,8 @@ def build_tools(prefix, product_name, occupation):
1749
1179
  },
1750
1180
  ),
1751
1181
  )
1182
+ if PRODUCT_ID == 'realtor' and os.getenv('TASKSAI_WORKSPACE_DIR','').strip():
1183
+ tools.append(Tool(name=f'{prefix}_workspace',description='Open your local TasksAI workspace to list, reopen, or create projects from previously selected skills. This does not execute a skill or spend a TasksAI credit. Use for saved projects and after a workspace update.',inputSchema={'type':'object','properties':{},'additionalProperties':False}))
1752
1184
  return tools
1753
1185
 
1754
1186
 
@@ -1848,7 +1280,7 @@ async def call_tool(name, arguments):
1848
1280
  # requests. Preserve privacy if a host bypasses startup and calls search or
1849
1281
  # categories directly: use only bundled identity and empty/local caches.
1850
1282
  if _vertical is None:
1851
- if name.endswith("_search") or name.endswith("_categories"):
1283
+ if name.endswith("_search") or name.endswith("_categories") or name.endswith("_workspace"):
1852
1284
  _vertical = fallback_vertical(PRODUCT_ID)
1853
1285
  else:
1854
1286
  await load_vertical()
@@ -1862,6 +1294,12 @@ async def call_tool(name, arguments):
1862
1294
  occupation = v.get("occupation", "professionals")
1863
1295
 
1864
1296
  try:
1297
+ if name == f'{prefix}_workspace' and product_id == 'realtor':
1298
+ root=os.getenv('TASKSAI_WORKSPACE_DIR','').strip()
1299
+ if not root:return [TextContent(type='text',text='The local workspace is not enabled in this installation.')]
1300
+ from workflows.workspace_launch import launch
1301
+ url=await asyncio.to_thread(launch,root,'local-customer')
1302
+ return [TextContent(type='text',text=f'[Open your saved TasksAI projects]({url}). Opening saved work does not spend a TasksAI credit.')]
1865
1303
  # ── Search ────────────────────────────────────────────────────────────
1866
1304
  if name == f"{prefix}_search":
1867
1305
  # Discovery is snapshot-only. A user's raw prompt never causes a
@@ -1930,9 +1368,30 @@ async def call_tool(name, arguments):
1930
1368
  content = result.get("schema", result.get("content", ""))
1931
1369
  skill_name = result.get("skill_name", skill_id)
1932
1370
  credits_remaining = result.get("credits_remaining", "?")
1371
+ # Capture only an already-authorized delivery. Never execute again
1372
+ # to initialize local workflow storage, and never hide the paid
1373
+ # response if optional workspace capture is unavailable.
1374
+ workspace_notice = ""
1375
+ workspace_root = os.getenv("TASKSAI_WORKSPACE_DIR", "").strip()
1376
+ if product_id == "realtor" and workspace_root:
1377
+ try:
1378
+ from workflows.licensed_delivery import capture
1379
+ capture(workspace_root, result)
1380
+ try:
1381
+ from workflows.account_snapshot import remember
1382
+ remember(workspace_root,result)
1383
+ except Exception:pass
1384
+ from workflows.workspace_launch import launch
1385
+ workspace_url = launch(workspace_root, "local-customer")
1386
+ workspace_notice = f"\n[Open your local TasksAI workspace]({workspace_url}) to create or reopen a project.\n"
1387
+
1388
+ except Exception:
1389
+ workspace_notice = "\nThe local workspace could not open. The skill below remains available; do not execute it again just to open the workspace.\n"
1390
+
1933
1391
 
1934
1392
  return [TextContent(type="text", text=(
1935
1393
  f"# {skill_name}\n\n"
1394
+ f"{workspace_notice}"
1936
1395
  f"{content}\n\n"
1937
1396
  f"---\n"
1938
1397
  f"**Local document output:** After you produce the user's final deliverable, call "
@@ -1963,6 +1422,13 @@ async def call_tool(name, arguments):
1963
1422
  "Controlled live research was unavailable. Continue only with intake or professional-supplied current official sources; do not substitute another jurisdiction."
1964
1423
  )
1965
1424
  result["instructions"] = instructions
1425
+ workspace_root = os.getenv("TASKSAI_WORKSPACE_DIR", "").strip()
1426
+ if product_id == "realtor" and workspace_root:
1427
+ try:
1428
+ from workflows.authority_guides import remember
1429
+ remember(workspace_root, (arguments or {}).get("skill_id", ""), result)
1430
+ except Exception:
1431
+ pass # Optional local capture must not hide the source lookup result.
1966
1432
  return [TextContent(type="text", text=format_jurisdiction_source_pack(result))]
1967
1433
 
1968
1434
  # ── Save Document ───────────────────────────────────────────────────
@@ -1985,6 +1451,11 @@ async def call_tool(name, arguments):
1985
1451
  result = await api_get("/v1/credits/balance")
1986
1452
  balance = result.get("credits_balance", "?")
1987
1453
  lic_type = result.get("license_type", "")
1454
+ if product_id=='realtor' and os.getenv('TASKSAI_WORKSPACE_DIR','').strip():
1455
+ try:
1456
+ from workflows.account_snapshot import remember
1457
+ remember(os.environ['TASKSAI_WORKSPACE_DIR'],result)
1458
+ except Exception:pass # Optional local display must not hide the balance response.
1988
1459
  domain = v.get("domain", "farmertasksai.com")
1989
1460
  return [TextContent(type="text", text=(
1990
1461
  f"**{product_name} Credits**\n\n"
@@ -2067,4 +1538,7 @@ async def main():
2067
1538
 
2068
1539
 
2069
1540
  if __name__ == "__main__":
2070
- asyncio.run(main())
1541
+ from pathlib import Path
1542
+ from software_use import software_use
1543
+ with software_use(Path(__file__).resolve().parent.parent):
1544
+ asyncio.run(main())