rsi-boot 0.1.0__py3-none-any.whl

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 (183) hide show
  1. rsi_boot/__init__.py +3 -0
  2. rsi_boot/__main__.py +496 -0
  3. rsi_boot/api/__init__.py +0 -0
  4. rsi_boot/api/mcp_server.py +155 -0
  5. rsi_boot/api/tools/__init__.py +0 -0
  6. rsi_boot/api/tools/conflicts_tool.py +104 -0
  7. rsi_boot/api/tools/explore_tool.py +222 -0
  8. rsi_boot/api/tools/feedback_tool.py +87 -0
  9. rsi_boot/api/tools/impact_tool.py +64 -0
  10. rsi_boot/api/tools/knowledge_add_tool.py +124 -0
  11. rsi_boot/api/tools/knowledge_delete_tool.py +33 -0
  12. rsi_boot/api/tools/knowledge_review_tool.py +253 -0
  13. rsi_boot/api/tools/knowledge_search_tool.py +50 -0
  14. rsi_boot/api/tools/knowledge_update_tool.py +64 -0
  15. rsi_boot/api/tools/learn_tool.py +134 -0
  16. rsi_boot/api/tools/memory_health_tool.py +79 -0
  17. rsi_boot/api/tools/memory_tool.py +67 -0
  18. rsi_boot/api/tools/query_tool.py +58 -0
  19. rsi_boot/api/tools/recall_tool.py +40 -0
  20. rsi_boot/api/tools/review_tool.py +92 -0
  21. rsi_boot/api/tools/stats_tool.py +72 -0
  22. rsi_boot/bootstrap.py +236 -0
  23. rsi_boot/cli/__init__.py +0 -0
  24. rsi_boot/cli/bootstrap_command.py +412 -0
  25. rsi_boot/cli/impact_command.py +93 -0
  26. rsi_boot/cli/knowledge_accept.py +246 -0
  27. rsi_boot/cli/learn_command.py +139 -0
  28. rsi_boot/cli/memory_command.py +216 -0
  29. rsi_boot/cli/progress.py +254 -0
  30. rsi_boot/cli/statusline_command.py +585 -0
  31. rsi_boot/cli/wipe_command.py +259 -0
  32. rsi_boot/common/__init__.py +0 -0
  33. rsi_boot/common/circuit_breaker.py +94 -0
  34. rsi_boot/common/logger.py +35 -0
  35. rsi_boot/common/stdio.py +37 -0
  36. rsi_boot/config/__init__.py +0 -0
  37. rsi_boot/config/default.yaml +60 -0
  38. rsi_boot/config/injection_blocklist.yaml +11 -0
  39. rsi_boot/config/intent_examples.yaml +83 -0
  40. rsi_boot/config/intent_rules.yaml +21 -0
  41. rsi_boot/config/loader.py +206 -0
  42. rsi_boot/config/models.yaml +31 -0
  43. rsi_boot/config/quality_rubric.yaml +18 -0
  44. rsi_boot/config/role_templates/bug_analysis.jinja2 +5 -0
  45. rsi_boot/config/role_templates/coverage_check.jinja2 +4 -0
  46. rsi_boot/config/role_templates/data_analysis.jinja2 +4 -0
  47. rsi_boot/config/role_templates/doc_review.jinja2 +5 -0
  48. rsi_boot/config/role_templates/prd_gen.jinja2 +5 -0
  49. rsi_boot/config/role_templates/req_analyze.jinja2 +4 -0
  50. rsi_boot/config/role_templates/stakeholder_comm.jinja2 +4 -0
  51. rsi_boot/config/role_templates/test_data.jinja2 +4 -0
  52. rsi_boot/config/role_templates/test_gen.jinja2 +5 -0
  53. rsi_boot/config/role_templates/test_review.jinja2 +5 -0
  54. rsi_boot/config/roles/pm.yaml +34 -0
  55. rsi_boot/config/roles/test.yaml +34 -0
  56. rsi_boot/config/templates/base.jinja2 +14 -0
  57. rsi_boot/config/templates/code_gen.jinja2 +4 -0
  58. rsi_boot/config/templates/code_review.jinja2 +8 -0
  59. rsi_boot/config/templates/debug.jinja2 +7 -0
  60. rsi_boot/config/templates/explain.jinja2 +7 -0
  61. rsi_boot/config/templates/general_assist.jinja2 +4 -0
  62. rsi_boot/core/config_merger.py +100 -0
  63. rsi_boot/core/exceptions.py +36 -0
  64. rsi_boot/core/masking.py +72 -0
  65. rsi_boot/core/models.py +207 -0
  66. rsi_boot/data/__init__.py +0 -0
  67. rsi_boot/data/migrate.py +71 -0
  68. rsi_boot/data/sqlite.py +155 -0
  69. rsi_boot/data/vec.py +147 -0
  70. rsi_boot/feedback/__init__.py +1 -0
  71. rsi_boot/feedback/implicit_tracker.py +229 -0
  72. rsi_boot/injector/__init__.py +5 -0
  73. rsi_boot/injector/blocklist.py +32 -0
  74. rsi_boot/injector/conflict.py +693 -0
  75. rsi_boot/injector/rule_injector.py +218 -0
  76. rsi_boot/injector/slug.py +14 -0
  77. rsi_boot/injector/targets.py +287 -0
  78. rsi_boot/knowledge/__init__.py +0 -0
  79. rsi_boot/knowledge/embedding.py +122 -0
  80. rsi_boot/knowledge/retriever.py +290 -0
  81. rsi_boot/learning/__init__.py +13 -0
  82. rsi_boot/learning/audit_store.py +195 -0
  83. rsi_boot/learning/dedup.py +164 -0
  84. rsi_boot/learning/failure_miner.py +81 -0
  85. rsi_boot/learning/gene_map.py +176 -0
  86. rsi_boot/learning/governance.py +61 -0
  87. rsi_boot/learning/knowledge_extractor.py +221 -0
  88. rsi_boot/learning/memory_health.py +757 -0
  89. rsi_boot/learning/pipeline.py +135 -0
  90. rsi_boot/learning/promote.py +192 -0
  91. rsi_boot/learning/proposal_engine.py +601 -0
  92. rsi_boot/learning/reading_pack_actions.py +267 -0
  93. rsi_boot/learning/regression_gate.py +253 -0
  94. rsi_boot/learning/retype_pack.py +86 -0
  95. rsi_boot/learning/skill_loader.py +115 -0
  96. rsi_boot/learning/snapshot_store.py +373 -0
  97. rsi_boot/learning/static_gate.py +73 -0
  98. rsi_boot/learning/teaching.py +433 -0
  99. rsi_boot/memory/__init__.py +20 -0
  100. rsi_boot/memory/graph.py +120 -0
  101. rsi_boot/memory/harvest.py +80 -0
  102. rsi_boot/memory/logstore.py +134 -0
  103. rsi_boot/memory/paths.py +38 -0
  104. rsi_boot/memory/store.py +300 -0
  105. rsi_boot/memory/tree.py +136 -0
  106. rsi_boot/memory/types.py +78 -0
  107. rsi_boot/memory/watch.py +79 -0
  108. rsi_boot/metrics/__init__.py +23 -0
  109. rsi_boot/metrics/footprint.py +115 -0
  110. rsi_boot/metrics/snapshot.py +84 -0
  111. rsi_boot/migrations/001_init.sql +138 -0
  112. rsi_boot/migrations/002_extraction.sql +25 -0
  113. rsi_boot/migrations/003_rule_artifacts.sql +22 -0
  114. rsi_boot/migrations/004_rule_conflicts.sql +23 -0
  115. rsi_boot/model/__init__.py +0 -0
  116. rsi_boot/model/adapter.py +120 -0
  117. rsi_boot/model/providers/__init__.py +0 -0
  118. rsi_boot/model/providers/anthropic.py +81 -0
  119. rsi_boot/model/providers/base.py +30 -0
  120. rsi_boot/model/providers/deepseek.py +29 -0
  121. rsi_boot/model/providers/mock.py +34 -0
  122. rsi_boot/model/providers/openai.py +52 -0
  123. rsi_boot/model/registry.py +67 -0
  124. rsi_boot/orchestrator/__init__.py +0 -0
  125. rsi_boot/orchestrator/handlers/__init__.py +0 -0
  126. rsi_boot/orchestrator/pipeline.py +273 -0
  127. rsi_boot/preprocessor/__init__.py +0 -0
  128. rsi_boot/preprocessor/intent_classifier.py +234 -0
  129. rsi_boot/project.py +295 -0
  130. rsi_boot/quality/__init__.py +5 -0
  131. rsi_boot/quality/assessor.py +194 -0
  132. rsi_boot/rag/__init__.py +14 -0
  133. rsi_boot/rag/chunker.py +47 -0
  134. rsi_boot/rag/index.py +221 -0
  135. rsi_boot/rag/query.py +107 -0
  136. rsi_boot/rag/retriever.py +22 -0
  137. rsi_boot/scanner/__init__.py +0 -0
  138. rsi_boot/scanner/code_scanner.py +222 -0
  139. rsi_boot/scanner/config_scanner.py +246 -0
  140. rsi_boot/scanner/conversation_scanner.py +69 -0
  141. rsi_boot/scanner/correlation_engine.py +114 -0
  142. rsi_boot/scanner/document_scanner.py +230 -0
  143. rsi_boot/scanner/git_analyzer.py +168 -0
  144. rsi_boot/scanner/git_fix.py +66 -0
  145. rsi_boot/scanner/incremental.py +377 -0
  146. rsi_boot/scanner/profile_generator.py +133 -0
  147. rsi_boot/scanner/reading_packs.py +743 -0
  148. rsi_boot/scanner/relearn_skill.md +22 -0
  149. rsi_boot/scanner/report.py +324 -0
  150. rsi_boot/scanner/rule_seed_scanner.py +81 -0
  151. rsi_boot/scanner/signal_discovery.py +217 -0
  152. rsi_boot/scanner/validator.py +98 -0
  153. rsi_boot/scanner/version_conflict.py +177 -0
  154. rsi_boot/scheduler/__init__.py +0 -0
  155. rsi_boot/scheduler/manager.py +152 -0
  156. rsi_boot/scheduler/tasks.py +91 -0
  157. rsi_boot/services/__init__.py +0 -0
  158. rsi_boot/services/decision_queue.py +357 -0
  159. rsi_boot/services/export_service.py +35 -0
  160. rsi_boot/services/impact_service.py +282 -0
  161. rsi_boot/services/knowledge_service.py +558 -0
  162. rsi_boot/services/legacy_migrate.py +306 -0
  163. rsi_boot/services/log_service.py +177 -0
  164. rsi_boot/services/memory_migrate.py +634 -0
  165. rsi_boot/services/profile_service.py +268 -0
  166. rsi_boot/services/recall_service.py +502 -0
  167. rsi_boot/services/stats_service.py +348 -0
  168. rsi_boot/strategy/__init__.py +0 -0
  169. rsi_boot/strategy/cost_router.py +71 -0
  170. rsi_boot/strategy/engine.py +139 -0
  171. rsi_boot/strategy/prompt_renderer.py +84 -0
  172. rsi_boot/strategy/recall.py +172 -0
  173. rsi_boot/ux/__init__.py +13 -0
  174. rsi_boot/ux/lang.py +102 -0
  175. rsi_boot/ux/messages.py +396 -0
  176. rsi_boot/ux/next.py +36 -0
  177. rsi_boot/ux/quips.py +685 -0
  178. rsi_boot/ux/terms.py +97 -0
  179. rsi_boot-0.1.0.dist-info/METADATA +251 -0
  180. rsi_boot-0.1.0.dist-info/RECORD +183 -0
  181. rsi_boot-0.1.0.dist-info/WHEEL +4 -0
  182. rsi_boot-0.1.0.dist-info/entry_points.txt +2 -0
  183. rsi_boot-0.1.0.dist-info/licenses/LICENSE +21 -0
rsi_boot/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """RSI Boot — 仓库绑定的 MCP(Harness-RSI 递归自我改进,记忆跟随项目、跨宿主可用)。"""
2
+
3
+ __version__ = "0.1.0"
rsi_boot/__main__.py ADDED
@@ -0,0 +1,496 @@
1
+ """rsi CLI(§7):init / serve / query / knowledge 子命令。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import json
8
+ import logging
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from .common.logger import setup_cli_logging, setup_logging
14
+ from .common.stdio import ensure_utf8_stdio
15
+ from .project import pinned_serve_root, resolve_project_root, resolve_serve_root
16
+ from .ux.lang import locale_lang
17
+ from .ux.messages import t
18
+
19
+ # 注意:.bootstrap / .core.models 不在这里顶层导入——它们会拖入 pydantic/openai 等重依赖,
20
+ # 拖慢 rsi statusline 这类宿主高频 spawn 的轻命令。各 handler 内部按需局部导入。
21
+
22
+
23
+ def _print_json(payload: object) -> None:
24
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
25
+ print(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
26
+
27
+
28
+ async def cmd_init(args: argparse.Namespace) -> int:
29
+ from .bootstrap import rsi_home
30
+
31
+ setup_cli_logging(getattr(args, "verbose", False))
32
+ home = rsi_home()
33
+ home.mkdir(parents=True, exist_ok=True)
34
+ (home / "skills").mkdir(exist_ok=True)
35
+
36
+ config_file = home / "config.yaml"
37
+ if not config_file.exists():
38
+ config_file.write_text(
39
+ "# RSI Boot 用户级配置(合并于包内 default.yaml 之上,§2.5)\n"
40
+ "# lang: zh # 界面语言 zh/en;优先级:命令行 --lang > RSI_LANG > 本键 > 系统语言\n"
41
+ "# statusline_style: mixed # 状态栏文案风格 mixed/serious/healing/geek/lucky/buddy\n"
42
+ "# 项目级覆盖:在 <工作区>/.rsi/config.yaml 写同名键(statusline_style/lang),优先级高于本文件\n"
43
+ "# v3.0 主链路零 API Key;以下增强项按需开启(Spec v3.0 附录 C):\n"
44
+ "# enhance:\n"
45
+ "# embedding: true # 混合检索\n"
46
+ "# extract_llm: true # LLM 知识提取\n"
47
+ "# model:\n"
48
+ "# api_key: env:OPENAI_API_KEY\n",
49
+ encoding="utf-8",
50
+ )
51
+
52
+ print(f"初始化完成:{home}(用户配置)。项目记忆在各工作目录 .rsi/,首次 serve/bootstrap 时创建")
53
+ return 0
54
+
55
+
56
+ def on_memory_yaml_change(runtime: object, _path: Path) -> None:
57
+ """Official-dir YAML change: rebuild index and rewrite injector (same as serve startup)."""
58
+ runtime.invalidate_index()
59
+ rewrite = runtime.injector.rewrite(runtime.project_id or "")
60
+ if asyncio.iscoroutine(rewrite) or asyncio.isfuture(rewrite):
61
+ try:
62
+ loop = asyncio.get_running_loop()
63
+ except RuntimeError:
64
+ asyncio.run(rewrite)
65
+ else:
66
+ loop.create_task(rewrite)
67
+
68
+
69
+ class ServeSession:
70
+ """serve 运行态持有器:runtime 延迟到首个 MCP 请求才构建,以便用 roots/list 定锚工作区。
71
+
72
+ 绑定顺序:`--project-root` / `RSI_PROJECT_ROOT`(pinned,显式契约)
73
+ > MCP roots(客户端声明的窗口工作区,协议级)
74
+ > IDE 注入环境变量 / cwd(fallback,进程启动时解析)。
75
+ 若 fallback 解析错误(如插件形态下 cwd 是 Cursor 用户目录),
76
+ 延迟构建保证错误的根不会落盘任何 .rsi/ 或注入产物。
77
+ """
78
+
79
+ def __init__(self, *, pinned: Path | None, fallback: Path, watch: bool) -> None:
80
+ self.pinned = pinned
81
+ self.fallback = Path(fallback).resolve()
82
+ self.watch = watch
83
+ self.runtime = None
84
+ self.root: Path | None = None
85
+ self._scheduler = None
86
+ self._tasks: list[asyncio.Task] = []
87
+ self._started = False
88
+ self._lock = asyncio.Lock()
89
+
90
+ async def ensure_started(self, server: object = None):
91
+ """首个 MCP 请求(list_tools / call_tool)时构建 runtime;幂等。"""
92
+ if self._started:
93
+ return self.runtime
94
+ async with self._lock:
95
+ if self._started:
96
+ return self.runtime
97
+ root = self.pinned or self.fallback
98
+ if self.pinned is None and server is not None:
99
+ root = await self._root_from_client(server)
100
+ await self._start(root)
101
+ self._started = True
102
+ return self.runtime
103
+
104
+ async def _root_from_client(self, server: object) -> Path:
105
+ """向客户端发 roots/list;客户端无 roots 能力或查询失败则保持 fallback。"""
106
+ try:
107
+ result = await server.request_context.session.list_roots()
108
+ except Exception:
109
+ logging.getLogger(__name__).info(
110
+ "客户端 roots 能力不可用,工作区保持环境解析:%s", self.fallback
111
+ )
112
+ return self.fallback
113
+ uris = [str(root.uri) for root in result.roots]
114
+ root = resolve_serve_root(uris, self.fallback)
115
+ if root != self.fallback:
116
+ logging.getLogger(__name__).info(
117
+ "工作区由 MCP roots 绑定:%s(环境解析为 %s)", root, self.fallback
118
+ )
119
+ return root
120
+
121
+ async def _start(self, root: Path) -> None:
122
+ from .bootstrap import build_runtime
123
+ from .memory.watch import YamlWatcher
124
+ from .scheduler.manager import SchedulerManager
125
+
126
+ logging.getLogger(__name__).info("工作区 %s → %s", root, root / ".rsi")
127
+ self.root = root
128
+ self.runtime = await build_runtime(project_root=root)
129
+ self.runtime.feedback_worker.start() # §4.2 异步反馈消费
130
+ await self.runtime.injector.rewrite(self.runtime.project_id or "")
131
+ archive_dir = root / ".rsi" / "archive"
132
+ self._scheduler = SchedulerManager(
133
+ archive_dir, store=self.runtime.store, profiles=self.runtime.profiles,
134
+ extractor_provider=lambda: self.runtime.extractor,
135
+ proposal_engine_provider=lambda: self.runtime.proposal_engine,
136
+ conflict_detector_provider=lambda: self.runtime.conflict_detector,
137
+ project_ids=[self.runtime.project_id] if self.runtime.project_id else [],
138
+ project_root=root,
139
+ )
140
+ self._scheduler.start()
141
+ self._tasks.append(asyncio.create_task(self.runtime.watcher.watch_loop()))
142
+ if self.watch:
143
+ watcher = YamlWatcher(
144
+ root / ".rsi" / "memory",
145
+ on_change=lambda path: on_memory_yaml_change(self.runtime, path),
146
+ )
147
+ self._tasks.append(asyncio.create_task(watcher.watch_loop()))
148
+
149
+ async def stop(self) -> None:
150
+ for task in self._tasks:
151
+ task.cancel()
152
+ self._tasks.clear()
153
+ if self._scheduler is not None:
154
+ await self._scheduler.stop()
155
+ self._scheduler = None
156
+ if self.runtime is not None:
157
+ await self.runtime.close()
158
+ self.runtime = None
159
+
160
+
161
+ async def run_serve(project_root: Path, *, watch: bool = False, pinned: Path | None = None) -> int:
162
+ """File-only serve: startup inject, optional YAML watch, no sqlite."""
163
+ from .api.mcp_server import serve
164
+
165
+ session = ServeSession(pinned=pinned, fallback=project_root, watch=watch)
166
+ try:
167
+ await serve(session)
168
+ finally:
169
+ await session.stop()
170
+ return 0
171
+
172
+
173
+ async def cmd_serve(args: argparse.Namespace) -> int:
174
+ setup_logging("INFO" if getattr(args, "verbose", False) else "WARNING")
175
+ explicit = Path(args.project_root) if getattr(args, "project_root", None) else None
176
+ pinned = pinned_serve_root(explicit)
177
+ project_root = resolve_project_root(explicit=explicit)
178
+ return await run_serve(project_root, watch=bool(getattr(args, "watch", False)), pinned=pinned)
179
+
180
+
181
+ async def cmd_recall(args: argparse.Namespace) -> int:
182
+ """调试:执行一次记忆召回(v3.0 起替代原 query 冒烟——主链路无生成环节)"""
183
+ from .bootstrap import build_runtime, detect_user_id
184
+
185
+ setup_cli_logging(getattr(args, "verbose", False))
186
+ runtime = await build_runtime(project_root=resolve_project_root())
187
+ try:
188
+ payload = await runtime.recall.recall(
189
+ args.task, runtime.project_id or args.project,
190
+ user_id=detect_user_id(), role=args.role,
191
+ )
192
+ _print_json(payload)
193
+ return 0
194
+ finally:
195
+ await runtime.close()
196
+
197
+
198
+ async def cmd_migrate(args: argparse.Namespace) -> int:
199
+ """把旧版 ~/.rsi/rsi.db 里的命名空间拆进当前项目库(default 须显式认领)"""
200
+ setup_cli_logging(getattr(args, "verbose", False))
201
+ from .project import legacy_shared_db_path
202
+ from .services.legacy_migrate import list_legacy_namespaces
203
+
204
+ if args.migrate_action == "status":
205
+ ns = await list_legacy_namespaces()
206
+ _print_json({"legacy_db": str(legacy_shared_db_path()), "namespaces": ns})
207
+ return 0
208
+ if args.migrate_action == "adopt":
209
+ print(t("MIGRATE_ADOPT_DEPRECATED", locale_lang()), file=sys.stderr)
210
+ return 3
211
+ print("未知 migrate 子命令", file=sys.stderr)
212
+ return 2
213
+
214
+
215
+ async def cmd_knowledge(args: argparse.Namespace) -> int:
216
+ from .bootstrap import build_runtime
217
+ from .core.models import KnowledgeItem
218
+
219
+ setup_cli_logging(getattr(args, "verbose", False))
220
+ runtime = await build_runtime(project_root=resolve_project_root())
221
+ try:
222
+ project = runtime.project_id or args.project
223
+ if args.knowledge_action == "add":
224
+ content = args.content
225
+ if args.file:
226
+ content = Path(args.file).read_text(encoding="utf-8")
227
+ if not content:
228
+ print("knowledge add 需要 --content 或 --file", file=sys.stderr)
229
+ return 2
230
+ item = KnowledgeItem(
231
+ project_id=project, title=args.title, content=content,
232
+ content_type=args.type,
233
+ tags=[t for t in (args.tags or "").split(",") if t],
234
+ roles=[r for r in (args.roles or "").split(",") if r],
235
+ )
236
+ item_id = await runtime.knowledge.add(item)
237
+ if isinstance(item_id, dict) and item_id.get("status") == "error":
238
+ print(item_id.get("message") or "invalid", file=sys.stderr)
239
+ return 1
240
+ print(f"已添加知识条目:{item_id}")
241
+ return 0
242
+ if args.knowledge_action == "list":
243
+ _print_json(await runtime.knowledge.list(project))
244
+ return 0
245
+ if args.knowledge_action == "delete":
246
+ ok = await runtime.knowledge.delete(args.id, project)
247
+ print("已删除" if ok else "未找到该条目")
248
+ return 0 if ok else 1
249
+ if args.knowledge_action == "accept":
250
+ from .cli.knowledge_accept import run_knowledge_accept
251
+
252
+ return await run_knowledge_accept(
253
+ runtime,
254
+ run_id=getattr(args, "run", None),
255
+ reject=bool(getattr(args, "reject", False)),
256
+ conflicts=getattr(args, "conflicts", None),
257
+ )
258
+ print("未知 knowledge 子命令", file=sys.stderr)
259
+ return 2
260
+ finally:
261
+ await runtime.close()
262
+
263
+
264
+ def _add_verbose(p: argparse.ArgumentParser) -> None:
265
+ p.add_argument("--verbose", action="store_true",
266
+ help="恢复 INFO 级日志并走 stderr(默认 WARNING + stdout,避免 PowerShell 红块)")
267
+
268
+
269
+ def build_parser() -> argparse.ArgumentParser:
270
+ loc = locale_lang()
271
+ parser = argparse.ArgumentParser(prog="rsi", description="RSI Boot:个人本地 MCP 智能助手")
272
+ parser.add_argument("--lang", choices=["zh", "en"], default=None, help=t("CLI_LANG_HELP", loc))
273
+ sub = parser.add_subparsers(dest="command", required=True)
274
+
275
+ p_init = sub.add_parser("init", help="初始化 ~/.rsi 用户配置目录")
276
+ _add_verbose(p_init)
277
+ p_serve = sub.add_parser("serve", help="启动 MCP stdio server")
278
+ p_serve.add_argument(
279
+ "--project-root", default=None,
280
+ help="工作区根(全局 MCP 请用环境变量 RSI_PROJECT_ROOT;缺省读 WORKSPACE_FOLDER_PATHS / cwd)",
281
+ )
282
+ p_serve.add_argument("--watch", action="store_true", help="文件监听静默学习(§10.9.5)")
283
+ _add_verbose(p_serve) # 默认 WARNING;--verbose 才 INFO(Cursor 把 stderr INFO 标成 error)
284
+
285
+ p_boot = sub.add_parser("bootstrap", help="项目自学习:扫描信号源生成知识与画像(§10.9)")
286
+ p_boot.add_argument("--scope", default="", help="逗号分隔:docs,code,git,conversation,config")
287
+ p_boot.add_argument("--project-root", default=".", help="项目根目录,默认当前目录")
288
+ p_boot.add_argument("--dry-run", action="store_true", help="预览扫描计划,不写入")
289
+ p_boot.add_argument("--consent", action="store_true", help="显式同意扫描对话上下文")
290
+ p_boot.add_argument("--then-start", action="store_true", help="学习完成后启动服务")
291
+ p_boot.add_argument("--force", action="store_true", help="忽略 manifest 指纹全量重学")
292
+ p_boot.add_argument("--strict", action="store_true", help="严格模式:任何验证失败即中断")
293
+ p_boot.add_argument("--allow-sensitive", action="store_true", help="敏感内容脱敏后写入(默认跳过)")
294
+ p_boot.add_argument("--max-file-size", default="1MB", help="单文件大小上限,如 1MB")
295
+ p_boot.add_argument("--max-commits", type=int, default=500, help="Git 历史上限(P2.6 生效)")
296
+ p_boot.add_argument(
297
+ "--include", action="append", default=[],
298
+ help="把默认排除的目录加回学习,可重复或逗号分隔;"
299
+ "按目录名匹配(artifacts 会加回任意名为 artifacts 的文件夹)",
300
+ )
301
+ p_boot.add_argument("--host-judge", action="store_true",
302
+ help="对话内由宿主模型裁决冲突工作包(agent 用)")
303
+ p_boot.add_argument("--local-judge", action="store_true",
304
+ help="本地整条知识比对裁决冲突(人类 CLI 用)")
305
+ _add_verbose(p_boot)
306
+
307
+ p_recall = sub.add_parser("recall", help="调试:执行一次记忆召回(禁止项置顶 + 相关经验)")
308
+ p_recall.add_argument("task", help="任务描述")
309
+ p_recall.add_argument("--project", default=None, help="已废弃:由当前目录绑定")
310
+ p_recall.add_argument("--role", default=None, help="调用方角色")
311
+ _add_verbose(p_recall)
312
+
313
+ p_kn = sub.add_parser("knowledge", help="知识库管理")
314
+ _add_verbose(p_kn)
315
+ kn_sub = p_kn.add_subparsers(dest="knowledge_action", required=True)
316
+ p_add = kn_sub.add_parser("add", help="添加知识条目")
317
+ p_add.add_argument("--title", required=True)
318
+ p_add.add_argument("--content", default=None)
319
+ p_add.add_argument("--file", default=None, help="从文件读取内容(UTF-8)")
320
+ p_add.add_argument("--type", default="convention", dest="type",
321
+ choices=["convention", "prohibition", "documentation", "experience"],
322
+ help="内容类型(prohibition=禁止项,召回置顶并常驻注入)")
323
+ p_add.add_argument("--tags", default="", help="逗号分隔")
324
+ p_add.add_argument("--roles", default="", help="逗号分隔,空=通用")
325
+ p_add.add_argument("--project", default=None, help="已废弃:由当前目录绑定")
326
+ _add_verbose(p_add)
327
+ p_list = kn_sub.add_parser("list", help="列出知识条目")
328
+ p_list.add_argument("--project", default=None, help="已废弃:由当前目录绑定")
329
+ _add_verbose(p_list)
330
+ p_del = kn_sub.add_parser("delete", help="删除知识条目")
331
+ p_del.add_argument("id")
332
+ p_del.add_argument("--project", default=None, help="已废弃:由当前目录绑定")
333
+ _add_verbose(p_del)
334
+ p_acc = kn_sub.add_parser("accept", help="放行本轮 bootstrap 抽取(不碰 auto-extract)")
335
+ p_acc.add_argument("--reject", action="store_true", help="拒绝本轮抽取")
336
+ p_acc.add_argument(
337
+ "--conflicts", choices=["tend", "coexist"], default=None,
338
+ help="tend=按 recommended 裁决本轮冲突;coexist=两侧都留",
339
+ )
340
+ p_acc.add_argument("--run", default=None, help="bootstrap run id;缺省读 .rsi/bootstrap_run.json")
341
+ _add_verbose(p_acc)
342
+
343
+ p_wipe = sub.add_parser("wipe", help="一键清除本项目记忆文件(保留 identity.json)")
344
+ p_wipe.add_argument(
345
+ "--yes", action="store_true",
346
+ help="确认删除 .rsi 下记忆文件与缓存(保留 identity.json)",
347
+ )
348
+ p_wipe.add_argument(
349
+ "--project-root", default=None,
350
+ help="项目根,默认当前工作区",
351
+ )
352
+ _add_verbose(p_wipe)
353
+
354
+ p_mig = sub.add_parser("migrate", help="从旧版 ~/.rsi/rsi.db 认领命名空间到当前项目")
355
+ _add_verbose(p_mig)
356
+ mig_sub = p_mig.add_subparsers(dest="migrate_action", required=True)
357
+ mig_sub.add_parser("status", help="列出遗留共享库中的 project_id 命名空间")
358
+ p_adopt = mig_sub.add_parser(
359
+ "adopt",
360
+ help="把指定命名空间拷入当前项目库(请改用 rsi memory migrate)",
361
+ )
362
+ p_adopt.add_argument("namespace", help="旧库中的 project_id,如 default 或目录名")
363
+ p_adopt.add_argument("--project-root", default=".", help="目标项目根,默认当前目录")
364
+ _add_verbose(p_adopt)
365
+
366
+ p_mem = sub.add_parser("memory", help=t("CLI_MEMORY_HELP", loc))
367
+ _add_verbose(p_mem)
368
+ p_mem.add_argument("argv", nargs=argparse.REMAINDER)
369
+
370
+ p_learn = sub.add_parser("learn", help=t("CLI_LEARN_HELP", loc))
371
+ _add_verbose(p_learn)
372
+ p_learn.add_argument("argv", nargs=argparse.REMAINDER)
373
+
374
+ p_impact = sub.add_parser(
375
+ "impact",
376
+ help="价值/影响力面板:注入成本、召回价值、采纳/合规、曲线与 A/B 省 token 计算(供宣讲)",
377
+ )
378
+ _add_verbose(p_impact)
379
+ p_impact.add_argument("argv", nargs=argparse.REMAINDER)
380
+
381
+ p_status = sub.add_parser(
382
+ "statusline",
383
+ help="宿主状态栏渲染(Claude Code / Cursor CLI statusLine,stdin 喂会话 JSON)",
384
+ )
385
+ # 显式注册而非 REMAINDER 透传:argparse 会拒绝子命令后未注册的选项(learn 同病),
386
+ # 宿主配置里的 command 字符串必须可靠可解析
387
+ p_status.add_argument("--lang", dest="statusline_lang", choices=["zh", "en"], default=None,
388
+ help="覆盖叙述语言(默认 RSI_LANG / 系统语言)")
389
+ p_status.add_argument("--no-rsi", dest="statusline_no_rsi", action="store_true",
390
+ help="不显示 RSI 统计段")
391
+ p_status.add_argument("--no-quip", dest="statusline_no_quip", action="store_true",
392
+ help="不显示随机文案")
393
+ p_status.add_argument("--plain", dest="statusline_plain", action="store_true",
394
+ help="禁用颜色与 emoji")
395
+ p_status.add_argument("--style", dest="statusline_style_flag",
396
+ choices=["mixed", "serious", "healing", "geek", "lucky", "buddy"],
397
+ default=None,
398
+ help="文案风格(默认 RSI_STATUSLINE_STYLE / config.yaml / mixed)")
399
+ p_status.add_argument("--validate-quips", dest="statusline_validate", nargs="?",
400
+ const="", default=None, metavar="PATH",
401
+ help="校验用户文案文件(默认 ~/.rsi/quips.json)后退出")
402
+ p_status.add_argument("argv", nargs=argparse.REMAINDER)
403
+ return parser
404
+
405
+
406
+ def main() -> None:
407
+ ensure_utf8_stdio()
408
+ args = build_parser().parse_args()
409
+ if getattr(args, "lang", None) in ("zh", "en"):
410
+ os.environ["RSI_LANG"] = args.lang
411
+ handlers = {
412
+ "init": cmd_init,
413
+ "serve": cmd_serve,
414
+ "recall": cmd_recall,
415
+ "knowledge": cmd_knowledge,
416
+ "migrate": cmd_migrate,
417
+ }
418
+ if args.command == "wipe":
419
+ from .cli.wipe_command import run_wipe
420
+
421
+ setup_cli_logging(getattr(args, "verbose", False))
422
+ try:
423
+ sys.exit(run_wipe(args))
424
+ except KeyboardInterrupt:
425
+ sys.exit(130)
426
+
427
+ if args.command == "memory":
428
+ from .cli.memory_command import run_memory
429
+
430
+ setup_cli_logging(getattr(args, "verbose", False))
431
+ try:
432
+ sys.exit(run_memory(list(getattr(args, "argv", []) or [])))
433
+ except KeyboardInterrupt:
434
+ sys.exit(130)
435
+
436
+ if args.command == "learn":
437
+ from .cli.learn_command import run_learn
438
+
439
+ setup_cli_logging(getattr(args, "verbose", False))
440
+ try:
441
+ sys.exit(run_learn(list(getattr(args, "argv", []) or [])))
442
+ except KeyboardInterrupt:
443
+ sys.exit(130)
444
+
445
+ if args.command == "impact":
446
+ from .cli.impact_command import run_impact
447
+
448
+ setup_cli_logging(getattr(args, "verbose", False))
449
+ try:
450
+ sys.exit(run_impact(list(getattr(args, "argv", []) or [])))
451
+ except KeyboardInterrupt:
452
+ sys.exit(130)
453
+
454
+ if args.command == "statusline":
455
+ from .cli.statusline_command import run_statusline
456
+
457
+ argv = list(getattr(args, "argv", []) or [])
458
+ if getattr(args, "statusline_lang", None):
459
+ argv += ["--lang", args.statusline_lang]
460
+ if getattr(args, "statusline_style_flag", None):
461
+ argv += ["--style", args.statusline_style_flag]
462
+ for flag, dest in (
463
+ ("--no-rsi", "statusline_no_rsi"),
464
+ ("--no-quip", "statusline_no_quip"),
465
+ ("--plain", "statusline_plain"),
466
+ ):
467
+ if getattr(args, dest, False):
468
+ argv.append(flag)
469
+ if getattr(args, "statusline_validate", None) is not None:
470
+ argv.append("--validate-quips")
471
+ if args.statusline_validate:
472
+ argv.append(args.statusline_validate)
473
+ try:
474
+ # 宿主高频 spawn:不走 setup_cli_logging(避免 stderr 噪音被标成 error)
475
+ sys.exit(run_statusline(argv))
476
+ except KeyboardInterrupt:
477
+ sys.exit(130)
478
+
479
+ if args.command == "bootstrap":
480
+ from .cli.bootstrap_command import run_bootstrap
481
+
482
+ setup_cli_logging(getattr(args, "verbose", False))
483
+ try:
484
+ code = asyncio.run(run_bootstrap(args))
485
+ except KeyboardInterrupt:
486
+ code = 130
487
+ sys.exit(code)
488
+ try:
489
+ code = asyncio.run(handlers[args.command](args))
490
+ except KeyboardInterrupt:
491
+ code = 130
492
+ sys.exit(code)
493
+
494
+
495
+ if __name__ == "__main__":
496
+ main()
File without changes
@@ -0,0 +1,155 @@
1
+ """MCP stdio server(Spec v3.0 §6):对外暴露 rsi_recall / rsi_feedback / rsi_knowledge_* /
2
+ rsi_review / rsi_conflicts / rsi_memory_health / rsi_stats / rsi_learn / rsi_memory 工具族。
3
+ v3.0 起 rsi_query 退役(主链路无生成环节)。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ from typing import Any
11
+
12
+ import mcp.types as types
13
+ from mcp.server import Server
14
+ from mcp.server.stdio import stdio_server
15
+
16
+ from .. import __version__
17
+ from ..services.stats_service import StatsService
18
+ from ..ux.messages import TOOL_DESC
19
+ from .tools import (
20
+ conflicts_tool,
21
+ explore_tool,
22
+ feedback_tool,
23
+ impact_tool,
24
+ knowledge_add_tool,
25
+ knowledge_delete_tool,
26
+ knowledge_review_tool,
27
+ knowledge_search_tool,
28
+ knowledge_update_tool,
29
+ learn_tool,
30
+ memory_health_tool,
31
+ memory_tool,
32
+ recall_tool,
33
+ review_tool,
34
+ stats_tool,
35
+ )
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ _INSTRUCTIONS = TOOL_DESC["recall"]
40
+
41
+
42
+ def build_server(session: Any) -> Server:
43
+ """session 为 __main__.ServeSession(首个请求时按 MCP roots 定锚并构建 runtime)"""
44
+ # initialize 握手:声明版本与能力说明(§7 协议契约,P1.15)
45
+ server: Server = Server("rsi-boot", version=__version__, instructions=_INSTRUCTIONS)
46
+
47
+ @server.list_tools()
48
+ async def list_tools() -> list[types.Tool]:
49
+ # 握手后客户端立刻拉工具清单:借此时机定锚工作区并构建 runtime,
50
+ # 保证注入产物在 agent 读 AGENTS.md 前落盘;构建失败不阻塞工具清单返回。
51
+ try:
52
+ await session.ensure_started(server)
53
+ except Exception:
54
+ logger.exception("runtime 构建失败(list_tools 仍返回工具清单)")
55
+ return [
56
+ types.Tool(name=recall_tool.TOOL_NAME,
57
+ description=recall_tool.TOOL_DESCRIPTION,
58
+ inputSchema=recall_tool.INPUT_SCHEMA),
59
+ types.Tool(name=explore_tool.TOOL_NAME,
60
+ description=explore_tool.TOOL_DESCRIPTION,
61
+ inputSchema=explore_tool.INPUT_SCHEMA),
62
+ types.Tool(name=feedback_tool.TOOL_NAME,
63
+ description=feedback_tool.TOOL_DESCRIPTION,
64
+ inputSchema=feedback_tool.INPUT_SCHEMA),
65
+ types.Tool(name=knowledge_add_tool.TOOL_NAME,
66
+ description=knowledge_add_tool.TOOL_DESCRIPTION,
67
+ inputSchema=knowledge_add_tool.INPUT_SCHEMA),
68
+ types.Tool(name=knowledge_search_tool.TOOL_NAME,
69
+ description=knowledge_search_tool.TOOL_DESCRIPTION,
70
+ inputSchema=knowledge_search_tool.INPUT_SCHEMA),
71
+ types.Tool(name=knowledge_delete_tool.TOOL_NAME,
72
+ description=knowledge_delete_tool.TOOL_DESCRIPTION,
73
+ inputSchema=knowledge_delete_tool.INPUT_SCHEMA),
74
+ types.Tool(name=knowledge_update_tool.TOOL_NAME,
75
+ description=knowledge_update_tool.TOOL_DESCRIPTION,
76
+ inputSchema=knowledge_update_tool.INPUT_SCHEMA),
77
+ types.Tool(name=knowledge_review_tool.TOOL_NAME,
78
+ description=knowledge_review_tool.TOOL_DESCRIPTION,
79
+ inputSchema=knowledge_review_tool.INPUT_SCHEMA),
80
+ types.Tool(name=review_tool.TOOL_NAME,
81
+ description=review_tool.TOOL_DESCRIPTION,
82
+ inputSchema=review_tool.INPUT_SCHEMA),
83
+ types.Tool(name=conflicts_tool.TOOL_NAME,
84
+ description=conflicts_tool.TOOL_DESCRIPTION,
85
+ inputSchema=conflicts_tool.INPUT_SCHEMA),
86
+ types.Tool(name=stats_tool.TOOL_NAME,
87
+ description=stats_tool.TOOL_DESCRIPTION,
88
+ inputSchema=stats_tool.INPUT_SCHEMA),
89
+ types.Tool(name=impact_tool.TOOL_NAME,
90
+ description=impact_tool.TOOL_DESCRIPTION,
91
+ inputSchema=impact_tool.INPUT_SCHEMA),
92
+ types.Tool(name=learn_tool.TOOL_NAME,
93
+ description=learn_tool.TOOL_DESCRIPTION,
94
+ inputSchema=learn_tool.INPUT_SCHEMA),
95
+ types.Tool(name=memory_health_tool.TOOL_NAME,
96
+ description=memory_health_tool.TOOL_DESCRIPTION,
97
+ inputSchema=memory_health_tool.INPUT_SCHEMA),
98
+ types.Tool(name=memory_tool.TOOL_NAME,
99
+ description=memory_tool.TOOL_DESCRIPTION,
100
+ inputSchema=memory_tool.INPUT_SCHEMA),
101
+ ]
102
+
103
+ @server.call_tool()
104
+ async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
105
+ try:
106
+ runtime = await session.ensure_started(server)
107
+ logs = runtime.logs
108
+ feedback_secret = runtime.feedback_secret
109
+ if name == recall_tool.TOOL_NAME:
110
+ payload = await recall_tool.handle(runtime, arguments)
111
+ elif name == explore_tool.TOOL_NAME:
112
+ payload = await explore_tool.handle(runtime, arguments)
113
+ elif name == feedback_tool.TOOL_NAME:
114
+ payload = await feedback_tool.handle(logs, feedback_secret, arguments, worker=runtime.feedback_worker)
115
+ elif name == knowledge_add_tool.TOOL_NAME:
116
+ payload = await knowledge_add_tool.handle(runtime.knowledge, arguments)
117
+ elif name == knowledge_search_tool.TOOL_NAME:
118
+ payload = await knowledge_search_tool.handle(runtime.knowledge, arguments)
119
+ elif name == knowledge_delete_tool.TOOL_NAME:
120
+ payload = await knowledge_delete_tool.handle(runtime.knowledge, arguments)
121
+ elif name == knowledge_update_tool.TOOL_NAME:
122
+ payload = await knowledge_update_tool.handle(runtime.knowledge, arguments)
123
+ elif name == knowledge_review_tool.TOOL_NAME:
124
+ payload = await knowledge_review_tool.handle(runtime, arguments)
125
+ elif name == review_tool.TOOL_NAME:
126
+ payload = await review_tool.handle(runtime, arguments)
127
+ elif name == conflicts_tool.TOOL_NAME:
128
+ payload = await conflicts_tool.handle(runtime, arguments)
129
+ elif name == stats_tool.TOOL_NAME:
130
+ stats = StatsService(store=runtime.store)
131
+ stats.bound_project_id = runtime.project_id
132
+ payload = await stats_tool.handle(stats, arguments)
133
+ elif name == impact_tool.TOOL_NAME:
134
+ payload = await impact_tool.handle(runtime, arguments)
135
+ elif name == learn_tool.TOOL_NAME:
136
+ payload = await learn_tool.handle(runtime, arguments)
137
+ elif name == memory_health_tool.TOOL_NAME:
138
+ payload = await memory_health_tool.handle(runtime, arguments)
139
+ elif name == memory_tool.TOOL_NAME:
140
+ payload = await memory_tool.handle(runtime, arguments)
141
+ else:
142
+ payload = {"status": "error", "code": "invalid", "message": f"unknown tool: {name}"}
143
+ except Exception as exc: # 工具级兜底,避免 MCP 连接中断
144
+ logger.exception("tool %s failed", name)
145
+ payload = {"status": "error", "code": "invalid", "message": str(exc)}
146
+ return [types.TextContent(type="text", text=json.dumps(payload, ensure_ascii=False, default=str))]
147
+
148
+ return server
149
+
150
+
151
+ async def serve(session: Any) -> None:
152
+ """stdio 传输;runtime 由 session 在首个请求时按 roots 定锚后构建(随 session.stop 收尾)。"""
153
+ server = build_server(session)
154
+ async with stdio_server() as (read_stream, write_stream):
155
+ await server.run(read_stream, write_stream, server.create_initialization_options())
File without changes