sophhub 0.2.1 → 0.2.2

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 (187) hide show
  1. package/package.json +1 -1
  2. package/skills/compact-context/skill.json +20 -0
  3. package/skills/compact-context/src/SKILL.md +133 -0
  4. package/skills/compact-context/src/scripts/check.sh +381 -0
  5. package/skills/compact-context/src/scripts/set-keep-recent.mjs +1337 -0
  6. package/skills/compact-context/src/scripts/setup.sh +96 -0
  7. package/skills/feishu-notes-assistant-universal/skill.json +20 -0
  8. package/skills/feishu-notes-assistant-universal/src/README.md +55 -0
  9. package/skills/feishu-notes-assistant-universal/src/SKILL.md +159 -0
  10. package/skills/feishu-notes-assistant-universal/src/bin/linux-amd64/lark-cli-openclaw +0 -0
  11. package/skills/feishu-notes-assistant-universal/src/bin/linux-arm64/lark-cli-openclaw +0 -0
  12. package/skills/feishu-notes-assistant-universal/src/scripts/_resolve_lark_cli.py +58 -0
  13. package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_meeting_minutes.py +462 -0
  14. package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_notes_crud.py +547 -0
  15. package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_notes_crud_test.py +181 -0
  16. package/skills/feishu-notes-assistant-universal/src/scripts/run_meeting_minutes.py +80 -0
  17. package/skills/feishu-notes-assistant-universal/src/scripts/run_meeting_minutes.sh +5 -0
  18. package/skills/feishu-notes-assistant-universal/src/scripts/run_note_crud.py +32 -0
  19. package/skills/feishu-notes-assistant-universal/src/scripts/run_note_crud.sh +5 -0
  20. package/skills/image-classify/skill.json +5 -5
  21. package/skills/image-classify/src/SKILL.md +60 -67
  22. package/skills/image-classify/src/scripts/face_search.py +400 -15
  23. package/skills/image-classify/src/scripts/send_dm_message.py +332 -0
  24. package/skills/md2pdf-converter/skill.json +20 -0
  25. package/skills/md2pdf-converter/src/SKILL.md +244 -0
  26. package/skills/md2pdf-converter/src/_meta.json +6 -0
  27. package/skills/md2pdf-converter/src/scripts/generate_emoji_mapping.py +74 -0
  28. package/skills/md2pdf-converter/src/scripts/md2pdf-local.sh +291 -0
  29. package/skills/sophnet-bot-client/skill.json +20 -0
  30. package/skills/sophnet-bot-client/src/SKILL.md +255 -0
  31. package/skills/sophnet-bot-client/src/pyproject.toml +13 -0
  32. package/skills/sophnet-bot-client/src/scripts/__init__.py +0 -0
  33. package/skills/sophnet-bot-client/src/scripts/bot_client_proxy.py +165 -0
  34. package/skills/sophnet-bot-client/src/scripts/bot_client_safe.sh +29 -0
  35. package/skills/sophnet-bot-client/src/scripts/bot_client_setup.py +502 -0
  36. package/skills/sophnet-bot-client/src/tests/__init__.py +0 -0
  37. package/skills/sophnet-bot-client/src/tests/test_bot_client_proxy.py +255 -0
  38. package/skills/sophnet-bot-client/src/tests/test_bot_client_setup.py +679 -0
  39. package/skills/sophnet-bot-client/src/uv.lock +8 -0
  40. package/skills/sophnet-docx/skill.json +20 -0
  41. package/skills/sophnet-docx/src/SKILL.md +463 -0
  42. package/skills/sophnet-docx/src/package-lock.json +208 -0
  43. package/skills/sophnet-docx/src/package.json +16 -0
  44. package/skills/sophnet-docx/src/pyproject.toml +11 -0
  45. package/skills/sophnet-docx/src/scripts/__init__.py +1 -0
  46. package/skills/sophnet-docx/src/scripts/accept_changes.py +135 -0
  47. package/skills/sophnet-docx/src/scripts/comment.py +318 -0
  48. package/skills/sophnet-docx/src/scripts/ensure_uv_env.sh +68 -0
  49. package/skills/sophnet-docx/src/scripts/office/helpers/__init__.py +0 -0
  50. package/skills/sophnet-docx/src/scripts/office/helpers/merge_runs.py +199 -0
  51. package/skills/sophnet-docx/src/scripts/office/helpers/simplify_redlines.py +197 -0
  52. package/skills/sophnet-docx/src/scripts/office/pack.py +159 -0
  53. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  54. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  55. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  56. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  57. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  58. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  59. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  60. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  61. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  62. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  63. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  64. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  65. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  66. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  67. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  68. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  69. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  70. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  71. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  72. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  73. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  74. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  75. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  76. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  77. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  78. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  79. package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  80. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  81. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  82. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  83. package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  84. package/skills/sophnet-docx/src/scripts/office/schemas/mce/mc.xsd +75 -0
  85. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  86. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  87. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  88. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  89. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  90. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  91. package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  92. package/skills/sophnet-docx/src/scripts/office/soffice.py +183 -0
  93. package/skills/sophnet-docx/src/scripts/office/unpack.py +132 -0
  94. package/skills/sophnet-docx/src/scripts/office/validate.py +111 -0
  95. package/skills/sophnet-docx/src/scripts/office/validators/__init__.py +15 -0
  96. package/skills/sophnet-docx/src/scripts/office/validators/base.py +847 -0
  97. package/skills/sophnet-docx/src/scripts/office/validators/docx.py +446 -0
  98. package/skills/sophnet-docx/src/scripts/office/validators/pptx.py +275 -0
  99. package/skills/sophnet-docx/src/scripts/office/validators/redlining.py +247 -0
  100. package/skills/sophnet-docx/src/scripts/templates/comments.xml +3 -0
  101. package/skills/sophnet-docx/src/scripts/templates/commentsExtended.xml +3 -0
  102. package/skills/sophnet-docx/src/scripts/templates/commentsExtensible.xml +3 -0
  103. package/skills/sophnet-docx/src/scripts/templates/commentsIds.xml +3 -0
  104. package/skills/sophnet-docx/src/scripts/templates/people.xml +3 -0
  105. package/skills/sophnet-docx/src/scripts/upload_file.sh +96 -0
  106. package/skills/sophnet-docx/src/uv.lock +320 -0
  107. package/skills/sophnet-pdf/skill.json +20 -0
  108. package/skills/sophnet-pdf/src/SKILL.md +413 -0
  109. package/skills/sophnet-pdf/src/forms.md +297 -0
  110. package/skills/sophnet-pdf/src/pyproject.toml +14 -0
  111. package/skills/sophnet-pdf/src/reference.md +612 -0
  112. package/skills/sophnet-pdf/src/scripts/check_bounding_boxes.py +65 -0
  113. package/skills/sophnet-pdf/src/scripts/check_fillable_fields.py +11 -0
  114. package/skills/sophnet-pdf/src/scripts/convert_pdf_to_images.py +33 -0
  115. package/skills/sophnet-pdf/src/scripts/create_validation_image.py +37 -0
  116. package/skills/sophnet-pdf/src/scripts/enhance_tutorial.py +558 -0
  117. package/skills/sophnet-pdf/src/scripts/ensure_uv_env.sh +68 -0
  118. package/skills/sophnet-pdf/src/scripts/extract_form_field_info.py +122 -0
  119. package/skills/sophnet-pdf/src/scripts/extract_form_structure.py +115 -0
  120. package/skills/sophnet-pdf/src/scripts/extract_pdf_content.py +35 -0
  121. package/skills/sophnet-pdf/src/scripts/fill_fillable_fields.py +98 -0
  122. package/skills/sophnet-pdf/src/scripts/fill_pdf_form_with_annotations.py +107 -0
  123. package/skills/sophnet-pdf/src/scripts/upload_file.sh +88 -0
  124. package/skills/sophnet-pdf/src/uv.lock +537 -0
  125. package/skills/sophnet-xlsx/skill.json +20 -0
  126. package/skills/sophnet-xlsx/src/SKILL.md +399 -0
  127. package/skills/sophnet-xlsx/src/pyproject.toml +11 -0
  128. package/skills/sophnet-xlsx/src/scripts/ensure_uv_env.sh +68 -0
  129. package/skills/sophnet-xlsx/src/scripts/office/helpers/__init__.py +0 -0
  130. package/skills/sophnet-xlsx/src/scripts/office/helpers/merge_runs.py +199 -0
  131. package/skills/sophnet-xlsx/src/scripts/office/helpers/simplify_redlines.py +197 -0
  132. package/skills/sophnet-xlsx/src/scripts/office/pack.py +159 -0
  133. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  134. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  135. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  136. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  137. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  138. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  139. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  140. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  141. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  142. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  143. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  144. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  145. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  146. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  147. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  148. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  149. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  150. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  151. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  152. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  153. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  154. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  155. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  156. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  157. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  158. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  159. package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  160. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  161. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  162. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  163. package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  164. package/skills/sophnet-xlsx/src/scripts/office/schemas/mce/mc.xsd +75 -0
  165. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  166. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  167. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  168. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  169. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  170. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  171. package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  172. package/skills/sophnet-xlsx/src/scripts/office/soffice.py +183 -0
  173. package/skills/sophnet-xlsx/src/scripts/office/unpack.py +132 -0
  174. package/skills/sophnet-xlsx/src/scripts/office/validate.py +111 -0
  175. package/skills/sophnet-xlsx/src/scripts/office/validators/__init__.py +15 -0
  176. package/skills/sophnet-xlsx/src/scripts/office/validators/base.py +847 -0
  177. package/skills/sophnet-xlsx/src/scripts/office/validators/docx.py +446 -0
  178. package/skills/sophnet-xlsx/src/scripts/office/validators/pptx.py +275 -0
  179. package/skills/sophnet-xlsx/src/scripts/office/validators/redlining.py +247 -0
  180. package/skills/sophnet-xlsx/src/scripts/recalc.py +184 -0
  181. package/skills/sophnet-xlsx/src/scripts/upload_file.sh +96 -0
  182. package/skills/sophnet-xlsx/src/uv.lock +319 -0
  183. package/skills/wechat-article-publisher/skill.json +20 -0
  184. package/skills/wechat-article-publisher/src/SKILL.md +60 -0
  185. package/skills/wechat-article-publisher/src/config.json +7 -0
  186. package/skills/wechat-article-publisher/src/pyproject.toml +12 -0
  187. package/skills/wechat-article-publisher/src/scripts/publish_wechat.py +825 -0
@@ -0,0 +1,679 @@
1
+ """Tests for bot_client_setup.py — setup/remove lifecycle."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ import tempfile
7
+ import unittest
8
+
9
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
10
+
11
+ from bot_client_setup import (
12
+ CREDENTIALS_FILENAME,
13
+ SECTION_MARKER_END,
14
+ SECTION_MARKER_START,
15
+ SESSION_FILENAME,
16
+ _find_section,
17
+ generate_proxy_section,
18
+ generate_section,
19
+ remove,
20
+ setup,
21
+ )
22
+
23
+
24
+ class TestGenerateSection(unittest.TestCase):
25
+ """Test AGENTS.md section generation (raw mode)."""
26
+
27
+ def test_contains_markers(self):
28
+ section = generate_section(
29
+ url="https://example.com/bot-api/chat",
30
+ remote_agent_id="main",
31
+ agent_id="vip-reader",
32
+ credentials_path="data/bot_credentials.json",
33
+ )
34
+ self.assertIn(SECTION_MARKER_START, section)
35
+ self.assertIn(SECTION_MARKER_END, section)
36
+
37
+ def test_contains_credentials_path(self):
38
+ section = generate_section(
39
+ url="https://example.com/bot-api/chat",
40
+ remote_agent_id="main",
41
+ agent_id="vip-reader",
42
+ credentials_path="data/bot_credentials.json",
43
+ )
44
+ self.assertIn("data/bot_credentials.json", section)
45
+ self.assertNotIn("https://example.com/bot-api/chat", section)
46
+
47
+ def test_contains_agent_ids(self):
48
+ section = generate_section(
49
+ url="https://example.com/bot-api/chat",
50
+ remote_agent_id="custom-agent",
51
+ agent_id="vip-reader",
52
+ credentials_path="data/bot_credentials.json",
53
+ )
54
+ self.assertIn("custom-agent", section)
55
+ self.assertIn("vip-reader", section)
56
+
57
+ def test_contains_session_filename(self):
58
+ section = generate_section(
59
+ url="https://example.com/bot-api/chat",
60
+ remote_agent_id="main",
61
+ agent_id="vip-reader",
62
+ credentials_path="data/bot_credentials.json",
63
+ )
64
+ self.assertIn(SESSION_FILENAME, section)
65
+
66
+ def test_contains_error_handling(self):
67
+ section = generate_section(
68
+ url="https://example.com/bot-api/chat",
69
+ remote_agent_id="main",
70
+ agent_id="vip-reader",
71
+ credentials_path="data/bot_credentials.json",
72
+ )
73
+ self.assertIn("错误处理", section)
74
+ self.assertIn("401/403", section)
75
+
76
+
77
+ class TestGenerateProxySection(unittest.TestCase):
78
+ """Test AGENTS.md section generation (proxy mode)."""
79
+
80
+ def test_contains_markers(self):
81
+ section = generate_proxy_section(
82
+ remote_agent_id="intern-helper",
83
+ agent_id="test",
84
+ wrapper_path="/path/to/bot_client_safe.sh",
85
+ )
86
+ self.assertIn(SECTION_MARKER_START, section)
87
+ self.assertIn(SECTION_MARKER_END, section)
88
+
89
+ def test_contains_wrapper_path(self):
90
+ section = generate_proxy_section(
91
+ remote_agent_id="intern-helper",
92
+ agent_id="test",
93
+ wrapper_path="/path/to/bot_client_safe.sh",
94
+ )
95
+ self.assertIn("/path/to/bot_client_safe.sh", section)
96
+
97
+ def test_does_not_contain_curl_commands(self):
98
+ """Proxy section should not contain executable curl/python3 commands."""
99
+ section = generate_proxy_section(
100
+ remote_agent_id="intern-helper",
101
+ agent_id="test",
102
+ wrapper_path="/path/to/bot_client_safe.sh",
103
+ )
104
+ self.assertNotIn("curl -s", section)
105
+ self.assertNotIn("python3 -c", section)
106
+ self.assertNotIn("BOT_URL=", section)
107
+
108
+ def test_does_not_contain_credentials(self):
109
+ section = generate_proxy_section(
110
+ remote_agent_id="intern-helper",
111
+ agent_id="test",
112
+ wrapper_path="/path/to/bot_client_safe.sh",
113
+ )
114
+ self.assertNotIn("bot_credentials.json", section)
115
+ self.assertNotIn("BOT_SECRET", section)
116
+
117
+ def test_contains_agent_ids(self):
118
+ section = generate_proxy_section(
119
+ remote_agent_id="intern-helper",
120
+ agent_id="test",
121
+ wrapper_path="/path/to/bot_client_safe.sh",
122
+ )
123
+ self.assertIn("intern-helper", section)
124
+ self.assertIn("test", section)
125
+
126
+ def test_forbids_direct_access(self):
127
+ section = generate_proxy_section(
128
+ remote_agent_id="intern-helper",
129
+ agent_id="test",
130
+ wrapper_path="/path/to/bot_client_safe.sh",
131
+ )
132
+ self.assertIn("禁止", section)
133
+ self.assertIn("系统强制执行", section)
134
+
135
+
136
+ class TestFindSection(unittest.TestCase):
137
+ """Test section boundary detection."""
138
+
139
+ def test_no_section(self):
140
+ self.assertIsNone(_find_section("# Hello\nSome content."))
141
+
142
+ def test_finds_section(self):
143
+ content = f"before\n{SECTION_MARKER_START}\nmiddle\n{SECTION_MARKER_END}\nafter"
144
+ bounds = _find_section(content)
145
+ self.assertIsNotNone(bounds)
146
+ start, end = bounds
147
+ extracted = content[start:end]
148
+ self.assertTrue(extracted.startswith(SECTION_MARKER_START))
149
+ self.assertTrue(extracted.endswith(SECTION_MARKER_END))
150
+
151
+ def test_missing_end_marker(self):
152
+ content = f"before\n{SECTION_MARKER_START}\nmiddle\n"
153
+ self.assertIsNone(_find_section(content))
154
+
155
+
156
+ class TestSetup(unittest.TestCase):
157
+ """Test setup command (raw mode)."""
158
+
159
+ def setUp(self):
160
+ self.tmpdir = tempfile.mkdtemp()
161
+ self.workspace_root = os.path.join(self.tmpdir, "workspaces")
162
+ self.agent_id = "test-agent"
163
+ self.workspace = os.path.join(self.workspace_root, self.agent_id)
164
+ os.makedirs(self.workspace)
165
+ self.agents_md = os.path.join(self.workspace, "AGENTS.md")
166
+ with open(self.agents_md, "w") as f:
167
+ f.write("# Test Agent\n\nExisting content.\n")
168
+ os.makedirs(os.path.join(self.workspace, "data"))
169
+
170
+ def tearDown(self):
171
+ import shutil
172
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
173
+
174
+ def test_setup_success(self):
175
+ result = setup(
176
+ agent_id=self.agent_id,
177
+ url="https://example.com/bot-api/chat",
178
+ secret="test-secret",
179
+ remote_agent_id="main",
180
+ workspace_root=self.workspace_root,
181
+ )
182
+ self.assertEqual(result["status"], "ok")
183
+ self.assertEqual(result["agent_id"], self.agent_id)
184
+ self.assertEqual(result["url"], "https://example.com/bot-api/chat")
185
+
186
+ def test_agents_md_updated(self):
187
+ setup(
188
+ agent_id=self.agent_id,
189
+ url="https://example.com/bot-api/chat",
190
+ secret="test-secret",
191
+ remote_agent_id="main",
192
+ workspace_root=self.workspace_root,
193
+ )
194
+ with open(self.agents_md) as f:
195
+ content = f.read()
196
+ self.assertIn(SECTION_MARKER_START, content)
197
+ self.assertIn(SECTION_MARKER_END, content)
198
+ self.assertIn("Existing content.", content)
199
+
200
+ def test_credentials_file_created(self):
201
+ setup(
202
+ agent_id=self.agent_id,
203
+ url="https://example.com/bot-api/chat",
204
+ secret="test-secret",
205
+ remote_agent_id="main",
206
+ workspace_root=self.workspace_root,
207
+ )
208
+ creds_path = os.path.join(self.workspace, "data", CREDENTIALS_FILENAME)
209
+ self.assertTrue(os.path.isfile(creds_path))
210
+ with open(creds_path) as f:
211
+ creds = json.load(f)
212
+ self.assertEqual(creds["url"], "https://example.com/bot-api/chat")
213
+ self.assertEqual(creds["secret"], "test-secret")
214
+ self.assertEqual(creds["remote_agent_id"], "main")
215
+
216
+ def test_idempotent_setup(self):
217
+ """Running setup twice should replace, not duplicate."""
218
+ for _ in range(2):
219
+ setup(
220
+ agent_id=self.agent_id,
221
+ url="https://example.com/bot-api/chat",
222
+ secret="test-secret",
223
+ remote_agent_id="main",
224
+ workspace_root=self.workspace_root,
225
+ )
226
+ with open(self.agents_md) as f:
227
+ content = f.read()
228
+ self.assertEqual(content.count(SECTION_MARKER_START), 1)
229
+ self.assertEqual(content.count(SECTION_MARKER_END), 1)
230
+
231
+ def test_setup_updates_url(self):
232
+ """Second setup with different URL should update."""
233
+ setup(
234
+ agent_id=self.agent_id,
235
+ url="https://old.com/bot-api/chat",
236
+ secret="old-secret",
237
+ remote_agent_id="main",
238
+ workspace_root=self.workspace_root,
239
+ )
240
+ setup(
241
+ agent_id=self.agent_id,
242
+ url="https://new.com/bot-api/chat",
243
+ secret="new-secret",
244
+ remote_agent_id="main",
245
+ workspace_root=self.workspace_root,
246
+ )
247
+ creds_path = os.path.join(self.workspace, "data", CREDENTIALS_FILENAME)
248
+ with open(creds_path) as f:
249
+ creds = json.load(f)
250
+ self.assertEqual(creds["url"], "https://new.com/bot-api/chat")
251
+ self.assertEqual(creds["secret"], "new-secret")
252
+
253
+ def test_workspace_not_found(self):
254
+ result = setup(
255
+ agent_id="nonexistent",
256
+ url="https://example.com/bot-api/chat",
257
+ secret="s",
258
+ remote_agent_id="main",
259
+ workspace_root=self.workspace_root,
260
+ )
261
+ self.assertEqual(result["status"], "error")
262
+
263
+ def test_agents_md_not_found(self):
264
+ os.remove(self.agents_md)
265
+ result = setup(
266
+ agent_id=self.agent_id,
267
+ url="https://example.com/bot-api/chat",
268
+ secret="s",
269
+ remote_agent_id="main",
270
+ workspace_root=self.workspace_root,
271
+ )
272
+ self.assertEqual(result["status"], "error")
273
+
274
+ def test_data_dir_created_if_missing(self):
275
+ """If data/ doesn't exist, setup should create it."""
276
+ import shutil
277
+ shutil.rmtree(os.path.join(self.workspace, "data"))
278
+ result = setup(
279
+ agent_id=self.agent_id,
280
+ url="https://example.com/bot-api/chat",
281
+ secret="s",
282
+ remote_agent_id="main",
283
+ workspace_root=self.workspace_root,
284
+ )
285
+ self.assertEqual(result["status"], "ok")
286
+ self.assertTrue(os.path.isdir(os.path.join(self.workspace, "data")))
287
+
288
+
289
+ class TestSetupProxy(unittest.TestCase):
290
+ """Test setup command (proxy mode)."""
291
+
292
+ def setUp(self):
293
+ self.tmpdir = tempfile.mkdtemp()
294
+ self.workspace_root = os.path.join(self.tmpdir, "workspaces")
295
+ self.admin_dir = os.path.join(self.tmpdir, "admin")
296
+ self.exec_approvals = os.path.join(self.tmpdir, "exec-approvals.json")
297
+ self.agent_id = "proxy-test"
298
+ self.workspace = os.path.join(self.workspace_root, self.agent_id)
299
+ os.makedirs(self.workspace)
300
+ self.agents_md = os.path.join(self.workspace, "AGENTS.md")
301
+ with open(self.agents_md, "w") as f:
302
+ f.write("# Proxy Test Agent\n\nExisting content.\n")
303
+
304
+ def tearDown(self):
305
+ import shutil
306
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
307
+
308
+ def test_proxy_setup_success(self):
309
+ result = setup(
310
+ agent_id=self.agent_id,
311
+ url="https://example.com/bot-api/chat",
312
+ secret="proxy-secret",
313
+ remote_agent_id="intern-helper",
314
+ workspace_root=self.workspace_root,
315
+ mode="proxy",
316
+ admin_dir=self.admin_dir,
317
+ exec_approvals_path=self.exec_approvals,
318
+ )
319
+ self.assertEqual(result["status"], "ok")
320
+ self.assertEqual(result["mode"], "proxy")
321
+ self.assertTrue(result["exec_allowlist_updated"])
322
+
323
+ def test_proxy_credentials_in_admin_dir(self):
324
+ """Proxy mode stores credentials in admin_dir, not workspace."""
325
+ setup(
326
+ agent_id=self.agent_id,
327
+ url="https://example.com/bot-api/chat",
328
+ secret="proxy-secret",
329
+ remote_agent_id="intern-helper",
330
+ workspace_root=self.workspace_root,
331
+ mode="proxy",
332
+ admin_dir=self.admin_dir,
333
+ exec_approvals_path=self.exec_approvals,
334
+ )
335
+ admin_creds = os.path.join(self.admin_dir, self.agent_id, CREDENTIALS_FILENAME)
336
+ self.assertTrue(os.path.isfile(admin_creds))
337
+ with open(admin_creds) as f:
338
+ creds = json.load(f)
339
+ self.assertEqual(creds["url"], "https://example.com/bot-api/chat")
340
+ self.assertEqual(creds["secret"], "proxy-secret")
341
+
342
+ workspace_creds = os.path.join(self.workspace, "data", CREDENTIALS_FILENAME)
343
+ self.assertFalse(os.path.exists(workspace_creds))
344
+
345
+ def test_proxy_agents_md_no_curl_commands(self):
346
+ """Proxy mode AGENTS.md should not contain executable curl/python3 commands."""
347
+ setup(
348
+ agent_id=self.agent_id,
349
+ url="https://example.com/bot-api/chat",
350
+ secret="proxy-secret",
351
+ remote_agent_id="intern-helper",
352
+ workspace_root=self.workspace_root,
353
+ mode="proxy",
354
+ admin_dir=self.admin_dir,
355
+ exec_approvals_path=self.exec_approvals,
356
+ )
357
+ with open(self.agents_md) as f:
358
+ content = f.read()
359
+ self.assertIn(SECTION_MARKER_START, content)
360
+ self.assertIn("bot_client_safe.sh", content)
361
+ self.assertNotIn("curl -s", content)
362
+ self.assertNotIn("python3 -c", content)
363
+ self.assertNotIn("BOT_SECRET", content)
364
+ self.assertNotIn("BOT_URL=", content)
365
+
366
+ def test_proxy_agents_md_readonly(self):
367
+ """Proxy mode should set AGENTS.md to read-only."""
368
+ setup(
369
+ agent_id=self.agent_id,
370
+ url="https://example.com/bot-api/chat",
371
+ secret="proxy-secret",
372
+ remote_agent_id="intern-helper",
373
+ workspace_root=self.workspace_root,
374
+ mode="proxy",
375
+ admin_dir=self.admin_dir,
376
+ exec_approvals_path=self.exec_approvals,
377
+ )
378
+ mode = os.stat(self.agents_md).st_mode
379
+ self.assertFalse(mode & 0o200, "AGENTS.md should be read-only")
380
+
381
+ def test_proxy_exec_allowlist_updated(self):
382
+ """Proxy mode should add bot_client_safe.sh to exec allowlist."""
383
+ setup(
384
+ agent_id=self.agent_id,
385
+ url="https://example.com/bot-api/chat",
386
+ secret="proxy-secret",
387
+ remote_agent_id="intern-helper",
388
+ workspace_root=self.workspace_root,
389
+ mode="proxy",
390
+ admin_dir=self.admin_dir,
391
+ exec_approvals_path=self.exec_approvals,
392
+ )
393
+ self.assertTrue(os.path.isfile(self.exec_approvals))
394
+ with open(self.exec_approvals) as f:
395
+ data = json.load(f)
396
+ agent_entry = data["agents"][self.agent_id]
397
+ patterns = [e["pattern"] for e in agent_entry["allowlist"]]
398
+ self.assertTrue(
399
+ any("bot_client_safe.sh" in p for p in patterns),
400
+ f"bot_client_safe.sh not in allowlist: {patterns}",
401
+ )
402
+
403
+ def test_proxy_idempotent(self):
404
+ """Running proxy setup twice should not duplicate."""
405
+ for _ in range(2):
406
+ setup(
407
+ agent_id=self.agent_id,
408
+ url="https://example.com/bot-api/chat",
409
+ secret="proxy-secret",
410
+ remote_agent_id="intern-helper",
411
+ workspace_root=self.workspace_root,
412
+ mode="proxy",
413
+ admin_dir=self.admin_dir,
414
+ exec_approvals_path=self.exec_approvals,
415
+ )
416
+ with open(self.agents_md) as f:
417
+ content = f.read()
418
+ self.assertEqual(content.count(SECTION_MARKER_START), 1)
419
+
420
+ with open(self.exec_approvals) as f:
421
+ data = json.load(f)
422
+ patterns = [e["pattern"] for e in data["agents"][self.agent_id]["allowlist"]]
423
+ safe_sh_count = sum(1 for p in patterns if "bot_client_safe.sh" in p)
424
+ self.assertEqual(safe_sh_count, 1)
425
+
426
+ def test_proxy_handles_existing_readonly(self):
427
+ """Proxy setup should handle already read-only AGENTS.md."""
428
+ import stat as stat_mod
429
+ os.chmod(self.agents_md, stat_mod.S_IRUSR | stat_mod.S_IRGRP | stat_mod.S_IROTH)
430
+ result = setup(
431
+ agent_id=self.agent_id,
432
+ url="https://example.com/bot-api/chat",
433
+ secret="proxy-secret",
434
+ remote_agent_id="intern-helper",
435
+ workspace_root=self.workspace_root,
436
+ mode="proxy",
437
+ admin_dir=self.admin_dir,
438
+ exec_approvals_path=self.exec_approvals,
439
+ )
440
+ self.assertEqual(result["status"], "ok")
441
+
442
+
443
+ class TestRemove(unittest.TestCase):
444
+ """Test remove command."""
445
+
446
+ def setUp(self):
447
+ self.tmpdir = tempfile.mkdtemp()
448
+ self.workspace_root = os.path.join(self.tmpdir, "workspaces")
449
+ self.admin_dir = os.path.join(self.tmpdir, "admin")
450
+ self.agent_id = "test-agent"
451
+ self.workspace = os.path.join(self.workspace_root, self.agent_id)
452
+ os.makedirs(os.path.join(self.workspace, "data"))
453
+ self.agents_md = os.path.join(self.workspace, "AGENTS.md")
454
+ with open(self.agents_md, "w") as f:
455
+ f.write("# Test Agent\n\nExisting content.\n")
456
+
457
+ def tearDown(self):
458
+ import shutil
459
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
460
+
461
+ def _setup_raw(self):
462
+ setup(
463
+ agent_id=self.agent_id,
464
+ url="https://example.com/bot-api/chat",
465
+ secret="test-secret",
466
+ remote_agent_id="main",
467
+ workspace_root=self.workspace_root,
468
+ )
469
+ session_path = os.path.join(self.workspace, "data", SESSION_FILENAME)
470
+ with open(session_path, "w") as f:
471
+ f.write("session-key-123")
472
+
473
+ def test_remove_success(self):
474
+ self._setup_raw()
475
+ result = remove(
476
+ agent_id=self.agent_id,
477
+ workspace_root=self.workspace_root,
478
+ admin_dir=self.admin_dir,
479
+ )
480
+ self.assertEqual(result["status"], "ok")
481
+ self.assertTrue(result["removed_section"])
482
+
483
+ def test_section_removed_from_agents_md(self):
484
+ self._setup_raw()
485
+ remove(
486
+ agent_id=self.agent_id,
487
+ workspace_root=self.workspace_root,
488
+ admin_dir=self.admin_dir,
489
+ )
490
+ with open(self.agents_md) as f:
491
+ content = f.read()
492
+ self.assertNotIn(SECTION_MARKER_START, content)
493
+ self.assertNotIn(SECTION_MARKER_END, content)
494
+
495
+ def test_original_content_preserved(self):
496
+ with open(self.agents_md, "w") as f:
497
+ f.write("# Test Agent\n\nExisting content.\n")
498
+ self._setup_raw()
499
+ remove(
500
+ agent_id=self.agent_id,
501
+ workspace_root=self.workspace_root,
502
+ admin_dir=self.admin_dir,
503
+ )
504
+ with open(self.agents_md) as f:
505
+ content = f.read()
506
+ self.assertIn("Existing content.", content)
507
+
508
+ def test_credentials_and_session_deleted(self):
509
+ self._setup_raw()
510
+ result = remove(
511
+ agent_id=self.agent_id,
512
+ workspace_root=self.workspace_root,
513
+ admin_dir=self.admin_dir,
514
+ )
515
+ self.assertIn(CREDENTIALS_FILENAME, result["removed_files"])
516
+ self.assertIn(SESSION_FILENAME, result["removed_files"])
517
+ self.assertFalse(
518
+ os.path.isfile(os.path.join(self.workspace, "data", CREDENTIALS_FILENAME))
519
+ )
520
+ self.assertFalse(
521
+ os.path.isfile(os.path.join(self.workspace, "data", SESSION_FILENAME))
522
+ )
523
+
524
+ def test_remove_no_section(self):
525
+ with open(self.agents_md, "w") as f:
526
+ f.write("# Test Agent\n")
527
+ result = remove(
528
+ agent_id=self.agent_id,
529
+ workspace_root=self.workspace_root,
530
+ admin_dir=self.admin_dir,
531
+ )
532
+ self.assertEqual(result["status"], "ok")
533
+ self.assertFalse(result["removed_section"])
534
+
535
+ def test_remove_workspace_not_found(self):
536
+ result = remove(
537
+ agent_id="nonexistent",
538
+ workspace_root=self.workspace_root,
539
+ admin_dir=self.admin_dir,
540
+ )
541
+ self.assertEqual(result["status"], "error")
542
+
543
+
544
+ class TestRemoveProxy(unittest.TestCase):
545
+ """Test remove command for proxy mode setups."""
546
+
547
+ def setUp(self):
548
+ self.tmpdir = tempfile.mkdtemp()
549
+ self.workspace_root = os.path.join(self.tmpdir, "workspaces")
550
+ self.admin_dir = os.path.join(self.tmpdir, "admin")
551
+ self.exec_approvals = os.path.join(self.tmpdir, "exec-approvals.json")
552
+ self.agent_id = "proxy-remove-test"
553
+ self.workspace = os.path.join(self.workspace_root, self.agent_id)
554
+ os.makedirs(self.workspace)
555
+ with open(os.path.join(self.workspace, "AGENTS.md"), "w") as f:
556
+ f.write("# Proxy Remove Test\n")
557
+
558
+ def tearDown(self):
559
+ import shutil
560
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
561
+
562
+ def test_remove_proxy_credentials(self):
563
+ """Remove should clean up admin_dir credentials."""
564
+ setup(
565
+ agent_id=self.agent_id,
566
+ url="https://example.com/bot-api/chat",
567
+ secret="s",
568
+ remote_agent_id="main",
569
+ workspace_root=self.workspace_root,
570
+ mode="proxy",
571
+ admin_dir=self.admin_dir,
572
+ exec_approvals_path=self.exec_approvals,
573
+ )
574
+ admin_creds = os.path.join(self.admin_dir, self.agent_id, CREDENTIALS_FILENAME)
575
+ self.assertTrue(os.path.isfile(admin_creds))
576
+
577
+ result = remove(
578
+ agent_id=self.agent_id,
579
+ workspace_root=self.workspace_root,
580
+ admin_dir=self.admin_dir,
581
+ )
582
+ self.assertEqual(result["status"], "ok")
583
+ self.assertFalse(os.path.isfile(admin_creds))
584
+ self.assertIn(CREDENTIALS_FILENAME, result["removed_files"])
585
+
586
+
587
+ class TestCLI(unittest.TestCase):
588
+ """Test command-line interface via subprocess."""
589
+
590
+ def setUp(self):
591
+ self.tmpdir = tempfile.mkdtemp()
592
+ self.workspace_root = os.path.join(self.tmpdir, "workspaces")
593
+ self.admin_dir = os.path.join(self.tmpdir, "admin")
594
+ self.exec_approvals = os.path.join(self.tmpdir, "exec-approvals.json")
595
+ self.agent_id = "cli-test"
596
+ self.workspace = os.path.join(self.workspace_root, self.agent_id)
597
+ os.makedirs(os.path.join(self.workspace, "data"))
598
+ with open(os.path.join(self.workspace, "AGENTS.md"), "w") as f:
599
+ f.write("# CLI Test Agent\n")
600
+
601
+ def tearDown(self):
602
+ import shutil
603
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
604
+
605
+ def _run_cli(self, args: list[str]) -> tuple[dict, int]:
606
+ import subprocess
607
+ script = os.path.join(
608
+ os.path.dirname(__file__), "..", "scripts", "bot_client_setup.py"
609
+ )
610
+ result = subprocess.run(
611
+ [sys.executable, script, *args],
612
+ capture_output=True,
613
+ text=True,
614
+ )
615
+ output = json.loads(result.stdout) if result.stdout.strip() else {}
616
+ return output, result.returncode
617
+
618
+ def test_cli_setup_raw(self):
619
+ output, rc = self._run_cli([
620
+ "setup",
621
+ "--agent-id", self.agent_id,
622
+ "--url", "https://example.com/bot-api/chat",
623
+ "--secret", "cli-secret",
624
+ "--remote-agent-id", "main",
625
+ "--workspace-root", self.workspace_root,
626
+ ])
627
+ self.assertEqual(rc, 0)
628
+ self.assertEqual(output["status"], "ok")
629
+
630
+ def test_cli_setup_proxy(self):
631
+ output, rc = self._run_cli([
632
+ "setup",
633
+ "--agent-id", self.agent_id,
634
+ "--url", "https://example.com/bot-api/chat",
635
+ "--secret", "cli-secret",
636
+ "--remote-agent-id", "intern-helper",
637
+ "--mode", "proxy",
638
+ "--workspace-root", self.workspace_root,
639
+ "--admin-dir", self.admin_dir,
640
+ "--exec-approvals-path", self.exec_approvals,
641
+ ])
642
+ self.assertEqual(rc, 0)
643
+ self.assertEqual(output["status"], "ok")
644
+ self.assertEqual(output["mode"], "proxy")
645
+
646
+ def test_cli_remove(self):
647
+ self._run_cli([
648
+ "setup",
649
+ "--agent-id", self.agent_id,
650
+ "--url", "https://example.com/bot-api/chat",
651
+ "--secret", "cli-secret",
652
+ "--remote-agent-id", "main",
653
+ "--workspace-root", self.workspace_root,
654
+ ])
655
+ output, rc = self._run_cli([
656
+ "remove",
657
+ "--agent-id", self.agent_id,
658
+ "--workspace-root", self.workspace_root,
659
+ "--admin-dir", self.admin_dir,
660
+ ])
661
+ self.assertEqual(rc, 0)
662
+ self.assertEqual(output["status"], "ok")
663
+ self.assertTrue(output["removed_section"])
664
+
665
+ def test_cli_setup_missing_workspace(self):
666
+ output, rc = self._run_cli([
667
+ "setup",
668
+ "--agent-id", "nonexistent",
669
+ "--url", "https://example.com/bot-api/chat",
670
+ "--secret", "s",
671
+ "--remote-agent-id", "main",
672
+ "--workspace-root", self.workspace_root,
673
+ ])
674
+ self.assertEqual(rc, 1)
675
+ self.assertEqual(output["status"], "error")
676
+
677
+
678
+ if __name__ == "__main__":
679
+ unittest.main()
@@ -0,0 +1,8 @@
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "sophnet-bot-client"
7
+ version = "0.1.0"
8
+ source = { editable = "." }