@sakanano-moe/agenthelm 6.5.2 → 6.6.0

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 (109) hide show
  1. package/bin/agenthelm.js +1 -1
  2. package/package.json +2 -2
  3. package/runtime/index.json +80 -0
  4. package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +19 -0
  5. package/runtime/plugins/agent-pack/.mcp.json +12 -0
  6. package/runtime/plugins/agent-pack/hooks/hooks.json +16 -0
  7. package/runtime/plugins/agent-pack/mcp.json +14 -0
  8. package/runtime/plugins/agent-pack/plugin.json +20 -0
  9. package/runtime/plugins/agent-pack/references/capability-config.md +124 -0
  10. package/runtime/{agenthelm.py → plugins/agent-pack/runtime/agenthelm.py} +111 -28
  11. package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +388 -0
  12. package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +46 -0
  13. package/runtime/plugins/agents-init/.codex-plugin/plugin.json +22 -0
  14. package/runtime/plugins/agents-init/content.json +31 -0
  15. package/runtime/plugins/agents-init/plugin.json +19 -0
  16. package/runtime/plugins/agents-init/skills/agents-init/AGENT.template.md +376 -0
  17. package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +119 -0
  18. package/runtime/plugins/agents-init/skills/agents-init/index.html +357 -0
  19. package/runtime/plugins/agents-init/skills/agents-init/launcher.sh +93 -0
  20. package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +171 -0
  21. package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +1190 -0
  22. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_init_project.py +678 -0
  23. package/runtime/plugins/agents-init/skills/agents-init/skill.json +122 -0
  24. package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +26 -0
  25. package/runtime/plugins/opinion-manager/catalog/README.md +15 -0
  26. package/runtime/plugins/opinion-manager/catalog/rules/.gitkeep +0 -0
  27. package/runtime/plugins/opinion-manager/catalog/rules/om.align-requirements/versions/1.0.0.json +11 -0
  28. package/runtime/plugins/opinion-manager/catalog/rules/om.assumptions-authority/versions/1.0.0.json +11 -0
  29. package/runtime/plugins/opinion-manager/catalog/rules/om.batch-questions/versions/1.0.0.json +11 -0
  30. package/runtime/plugins/opinion-manager/catalog/rules/om.bilingual-terms/versions/1.0.0.json +11 -0
  31. package/runtime/plugins/opinion-manager/catalog/rules/om.calculations/versions/1.0.0.json +11 -0
  32. package/runtime/plugins/opinion-manager/catalog/rules/om.chinese-response/versions/1.0.0.json +11 -0
  33. package/runtime/plugins/opinion-manager/catalog/rules/om.clean-deliverable/versions/1.0.0.json +11 -0
  34. package/runtime/plugins/opinion-manager/catalog/rules/om.complete-options/versions/1.0.0.json +11 -0
  35. package/runtime/plugins/opinion-manager/catalog/rules/om.conclusion-first/versions/1.0.0.json +11 -0
  36. package/runtime/plugins/opinion-manager/catalog/rules/om.concrete-titles/versions/1.0.0.json +11 -0
  37. package/runtime/plugins/opinion-manager/catalog/rules/om.consistent-items/versions/1.0.0.json +11 -0
  38. package/runtime/plugins/opinion-manager/catalog/rules/om.device-readable/versions/1.0.0.json +11 -0
  39. package/runtime/plugins/opinion-manager/catalog/rules/om.editable-diagrams/versions/1.0.0.json +11 -0
  40. package/runtime/plugins/opinion-manager/catalog/rules/om.evidence-inference/versions/1.0.0.json +11 -0
  41. package/runtime/plugins/opinion-manager/catalog/rules/om.maintenance-chinese/versions/1.0.0.json +11 -0
  42. package/runtime/plugins/opinion-manager/catalog/rules/om.maintenance-project-language/versions/1.0.0.json +11 -0
  43. package/runtime/plugins/opinion-manager/catalog/rules/om.markdown-structure/versions/1.0.0.json +11 -0
  44. package/runtime/plugins/opinion-manager/catalog/rules/om.portable-files/versions/1.0.0.json +11 -0
  45. package/runtime/plugins/opinion-manager/catalog/rules/om.report-context/versions/1.0.0.json +11 -0
  46. package/runtime/plugins/opinion-manager/catalog/rules/om.report-formats/versions/1.0.0.json +11 -0
  47. package/runtime/plugins/opinion-manager/catalog/rules/om.report-review/versions/1.0.0.json +11 -0
  48. package/runtime/plugins/opinion-manager/catalog/rules/om.response-brief/versions/1.0.0.json +11 -0
  49. package/runtime/plugins/opinion-manager/catalog/rules/om.response-dense/versions/1.0.0.json +11 -0
  50. package/runtime/plugins/opinion-manager/catalog/rules/om.rules-tools/versions/1.0.0.json +11 -0
  51. package/runtime/plugins/opinion-manager/catalog/rules/om.semantic-wrap/versions/1.0.0.json +11 -0
  52. package/runtime/plugins/opinion-manager/catalog/rules/om.solutions-proportional/versions/1.0.0.json +11 -0
  53. package/runtime/plugins/opinion-manager/catalog/rules/om.solutions-three-level/versions/1.0.0.json +11 -0
  54. package/runtime/plugins/opinion-manager/catalog/rules/om.tables-compact/versions/1.0.0.json +11 -0
  55. package/runtime/plugins/opinion-manager/catalog/rules/om.tables-list-first/versions/1.0.0.json +11 -0
  56. package/runtime/plugins/opinion-manager/catalog/rules/om.test-evidence/versions/1.0.0.json +11 -0
  57. package/runtime/plugins/opinion-manager/catalog/rules/om.versioned-files/versions/1.0.0.json +11 -0
  58. package/runtime/plugins/opinion-manager/catalog/templates/.gitkeep +0 -0
  59. package/runtime/plugins/opinion-manager/catalog/templates/delivery-standard/compact-tables/versions/1.0.0.json +21 -0
  60. package/runtime/plugins/opinion-manager/catalog/templates/delivery-standard/list-first/versions/1.0.0.json +21 -0
  61. package/runtime/plugins/opinion-manager/catalog/templates/development-standard/chinese-maintenance/versions/1.0.0.json +15 -0
  62. package/runtime/plugins/opinion-manager/catalog/templates/development-standard/project-language/versions/1.0.0.json +15 -0
  63. package/runtime/plugins/opinion-manager/catalog/templates/response-style/brief-chinese/versions/1.0.0.json +19 -0
  64. package/runtime/plugins/opinion-manager/catalog/templates/response-style/dense-chinese/versions/1.0.0.json +19 -0
  65. package/runtime/plugins/opinion-manager/catalog/templates/scenario/concise-project/versions/1.0.0.json +39 -0
  66. package/runtime/plugins/opinion-manager/catalog/templates/scenario/reviewed-project/versions/1.0.0.json +39 -0
  67. package/runtime/plugins/opinion-manager/catalog/templates/work-method/proportional/versions/1.0.0.json +20 -0
  68. package/runtime/plugins/opinion-manager/catalog/templates/work-method/three-level/versions/1.0.0.json +20 -0
  69. package/runtime/plugins/opinion-manager/content.json +63 -0
  70. package/runtime/plugins/opinion-manager/plugin.json +18 -0
  71. package/runtime/plugins/opinion-manager/references/lock.schema.json +14 -0
  72. package/runtime/plugins/opinion-manager/references/onboarding.md +57 -0
  73. package/runtime/plugins/opinion-manager/references/profile.schema.json +56 -0
  74. package/runtime/plugins/opinion-manager/references/rule.schema.json +16 -0
  75. package/runtime/plugins/opinion-manager/references/template.schema.json +71 -0
  76. package/runtime/plugins/opinion-manager/references/versioned-template.schema.json +19 -0
  77. package/runtime/plugins/opinion-manager/references/versioning.md +263 -0
  78. package/runtime/plugins/opinion-manager/runtime/constants.py +7 -0
  79. package/runtime/plugins/opinion-manager/runtime/context.py +180 -0
  80. package/runtime/plugins/opinion-manager/runtime/lifecycle.py +395 -0
  81. package/runtime/plugins/opinion-manager/runtime/opinion_manager.py +419 -0
  82. package/runtime/plugins/opinion-manager/runtime/versioned.py +705 -0
  83. package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +62 -0
  84. package/runtime/plugins/opinion-manager/skills/opinion-manager/agents/openai.yaml +4 -0
  85. package/runtime/plugins/opinion-manager/templates/.gitkeep +1 -0
  86. package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +18 -0
  87. package/runtime/plugins/project-orchestrator/content.json +37 -0
  88. package/runtime/plugins/project-orchestrator/plugin.json +19 -0
  89. package/runtime/plugins/project-orchestrator/references/asset-synonyms.example.json +8 -0
  90. package/runtime/plugins/project-orchestrator/references/case-workspaces.md +159 -0
  91. package/runtime/plugins/project-orchestrator/references/case.schema.json +31 -0
  92. package/runtime/plugins/project-orchestrator/runtime/assets.py +1288 -0
  93. package/runtime/plugins/project-orchestrator/runtime/case.py +557 -0
  94. package/runtime/plugins/project-orchestrator/runtime/case_workspace.py +350 -0
  95. package/runtime/plugins/project-orchestrator/runtime/memory.py +363 -0
  96. package/runtime/plugins/project-orchestrator/runtime/project_base.py +107 -0
  97. package/runtime/plugins/project-orchestrator/runtime/project_cli.py +599 -0
  98. package/runtime/plugins/project-orchestrator/runtime/project_demo.py +504 -0
  99. package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +470 -0
  100. package/runtime/plugins/project-orchestrator/runtime/project_menu.py +394 -0
  101. package/runtime/plugins/project-orchestrator/runtime/project_real.py +522 -0
  102. package/runtime/plugins/project-orchestrator/runtime/project_write.py +329 -0
  103. package/runtime/plugins/project-orchestrator/runtime/wf.py +439 -0
  104. package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +230 -0
  105. package/runtime/plugins/project-orchestrator/skills/project-orchestrator/agents/openai.yaml +4 -0
  106. package/runtime/plugins/project-orchestrator/templates/CASE.md +39 -0
  107. /package/runtime/{agent_pack_config.py → plugins/agent-pack/runtime/agent_pack_config.py} +0 -0
  108. /package/runtime/{mcp_env_launcher.py → plugins/agent-pack/runtime/mcp_env_launcher.py} +0 -0
  109. /package/runtime/{mcp_server.py → plugins/agent-pack/runtime/mcp_server.py} +0 -0
@@ -0,0 +1,599 @@
1
+ #!/usr/bin/env python3
2
+ """项目入口:不需要 AI Agent 的项目自助菜单与命令。
3
+
4
+ agenthelm project 交互菜单(终端里);非终端时打印概览后退出,不等待输入
5
+ agenthelm project find <词…> 找资料:关键词、日期(2025-12、10月)、--type、--in;--open N 打开第 N 项
6
+ agenthelm project open <路径> 打开资料(--reveal 显示所在目录,--copy 复制路径)
7
+ agenthelm project refs <路径> 谁引用它 / 它引用谁
8
+ agenthelm project progress [词] 看进展:按状态分组;--status 只看一种,--all 含已完成
9
+ agenthelm project show <事项> 事项详情:编号、旧编号、别名或标题片段都可以
10
+ agenthelm project guide 项目指南
11
+ agenthelm project health 检查项目与环境;有必须处理的项时退出码为 1
12
+ agenthelm project fix [项…] 修复与更新:默认只预览,--apply 执行;--keep / --accept 选择定制文件
13
+ agenthelm project record <事项|new> 记录事项:默认只预览,--apply 写入
14
+ agenthelm project tools [工具] 项目自行声明的工具;--run --apply 执行
15
+ agenthelm project capabilities 各项能力当前是否可用、为什么
16
+
17
+ 项目根目录的启动器(缺省 ./menu)等同于 agenthelm project --project <项目根目录>。
18
+ 全局选项可写在任意位置:--project <目录>、--output text|json、--no-input、--offline。
19
+ --demo study|personal|research|blank 使用内置的虚构项目体验,可加 --demo-fault
20
+ index-broken|offline|runtime-old|conflict;演示结果都标注「演示」,不读写真实文件。
21
+
22
+ 退出码:0 成功;1 没有结果或有必须处理的项;2 用法错误;3 运行时与项目入口不兼容;
23
+ 4 能力当前不可用;5 写入冲突(别人刚改过);6 执行失败;130 中断。
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import sys
30
+ from pathlib import Path
31
+ from typing import Any, Callable
32
+
33
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
34
+ import project_demo # noqa: E402
35
+ from project_base import LAUNCHER_API, RUNTIME_API, EntryError # noqa: E402
36
+ from project_demo import DemoProvider # noqa: E402
37
+ from wf import S, clip, display_width, term_width # noqa: E402
38
+
39
+ Provider = Any # DemoProvider 或 project_real.RealProvider:方法与返回结构相同
40
+ # 经 ``agenthelm project`` 启动时,agent-pack 把自己的模块放在这里,供检查与更新使用。
41
+ HOST: Any = None
42
+
43
+ EXIT_OK, EXIT_ATTENTION, EXIT_USAGE, EXIT_INCOMPATIBLE, EXIT_UNAVAILABLE, EXIT_CONFLICT = 0, 1, 2, 3, 4, 5
44
+ EXIT_FAILED, EXIT_INTERRUPTED = 6, 130
45
+ EXIT_BY_CODE = {"usage": EXIT_USAGE, "not-found": EXIT_ATTENTION, "ambiguous": EXIT_ATTENTION,
46
+ "runtime-incompatible": EXIT_INCOMPATIBLE, "unavailable": EXIT_UNAVAILABLE,
47
+ "index-broken": EXIT_UNAVAILABLE, "not-a-project": EXIT_UNAVAILABLE,
48
+ "profile-invalid": EXIT_UNAVAILABLE, "offline": EXIT_UNAVAILABLE,
49
+ "conflict": EXIT_CONFLICT, "failed": EXIT_FAILED}
50
+ GROUPS = (("active", "进行中"), ("waiting", "等待"), ("captured", "待澄清"), ("suspended", "挂起"),
51
+ ("done", "已完成"), ("abandoned", "放弃"), ("other", "其他状态"))
52
+ GROUP_LABEL = dict(GROUPS)
53
+ CLOSED = {"done", "abandoned"}
54
+ LEVELS = (("must", "必须处理"), ("suggest", "建议处理"), ("update", "可选更新"), ("unavailable", "当前不可用"))
55
+ LEVEL_LABEL = dict(LEVELS)
56
+ DEMO_BANNER = "【演示数据 · 不读写真实文件】"
57
+
58
+
59
+ def hhmm(value: str | None) -> str:
60
+ """2026-10-24T21:30:00+08:00 -> 10-24 21:30"""
61
+ return (value or "")[5:16].replace("T", " ")
62
+
63
+
64
+ def pad(text: str, width: int) -> str:
65
+ return text + " " * max(0, width - display_width(text))
66
+
67
+
68
+ # ---------------------------------------------------------------- provider
69
+
70
+ def open_provider(args: argparse.Namespace) -> Provider:
71
+ if args.demo:
72
+ return DemoProvider(args.demo, args.demo_fault)
73
+ import project_real # noqa: PLC0415 - only real runs need the project tools
74
+
75
+ return project_real.RealProvider(args.project, args.project is not None, offline=args.offline,
76
+ host=HOST, via_launcher=args.via_launcher)
77
+
78
+
79
+ def check_entry_api(provider: Provider) -> None:
80
+ low, high = RUNTIME_API
81
+ if provider.entry_api > high:
82
+ raise EntryError(f"这个项目的入口需要接口版本 {provider.entry_api},当前 agenthelm 运行时只支持到 {high}",
83
+ "runtime-incompatible",
84
+ ["更新运行时:agenthelm runtime update --apply(联网)",
85
+ "离线:agenthelm runtime install --source <新版发布包> --apply,见 INSTALL.md「离线安装」",
86
+ "项目文件没有被改动"])
87
+ if provider.entry_api < low:
88
+ raise EntryError(f"这个项目的入口较旧(接口版本 {provider.entry_api}),当前运行时最低支持 {low}",
89
+ "runtime-incompatible", ["更新项目入口:agenthelm update --only content,按提示保留或采用"])
90
+
91
+
92
+ # ---------------------------------------------------------------- renderers (text)
93
+
94
+ def header(provider: Provider, title: str) -> list[str]:
95
+ banner = S.yellow(DEMO_BANNER) if provider.simulated else ""
96
+ return [f"{S.bold(provider.name)} · {title} {banner}".rstrip()]
97
+
98
+
99
+ def render_overview(provider: Provider) -> list[str]:
100
+ overview = provider.overview()
101
+ index = overview["index"]
102
+ if index["status"] == "broken":
103
+ index_text = S.red("资料索引损坏(可重建)")
104
+ elif index["status"] == "missing":
105
+ index_text = "资料索引尚未建立(首次查找时建立)"
106
+ else:
107
+ index_text = f"资料 {index['file_count']} 个(索引 {hhmm(index['built_at'])})"
108
+ counts = overview["items"]
109
+ items_text = " · ".join(f"{label} {counts[key]}" for key, label in GROUPS if counts.get(key) and key not in CLOSED)
110
+ health = overview["health"]
111
+ health_text = " · ".join(f"{health[key]} 项{label}" for key, label in LEVELS if health.get(key)) or "未发现问题"
112
+ if not overview.get("health_checked", True):
113
+ health_text = "尚未检查(首页第 4 项)"
114
+ banner = S.yellow(DEMO_BANNER) if provider.simulated else ""
115
+ return [f"{S.bold(S.cyan(overview['name']))} {S.dim(overview['kind_label'])} {banner}".rstrip(),
116
+ f"{index_text} · 事项 {items_text or '暂无'}",
117
+ f"检查:{health_text}"]
118
+
119
+
120
+ def render_find(result: dict) -> list[str]:
121
+ width = term_width()
122
+ lines = []
123
+ if not result["results"]:
124
+ return [S.yellow("没有找到。") + S.dim("试试更短的词、去掉日期,或先在「看进展」里找到事项。")]
125
+ for number, item in enumerate(result["results"], 1):
126
+ label = item["title"] or item["name"]
127
+ date = hhmm(item["modified"])
128
+ left = f" {S.bold(S.cyan(f'{number:>2}'))} {S.bold(clip(label, width - 24))}"
129
+ lines.append(pad(left, width - len(date)) + S.dim(date))
130
+ owner = item["case_title"] or ("项目根目录" if item["container"] == "project" else f"目录 {item['container']}/")
131
+ status = GROUP_LABEL.get(item["status"], item["status"]) if item["status"] else ""
132
+ owner_text = f"{owner}({status})" if status else owner
133
+ refs = f" · 被引用 {item['referenced_by']} 处" if item["referenced_by"] else ""
134
+ lines.append(f" 归属:{clip(owner_text, width - 30)}{refs}")
135
+ lines.append(S.dim(f" {clip(item['path'], width - 6)}"))
136
+ if item["versions"]:
137
+ lines.append(S.dim(f" └ 同系列另有 {len(item['versions'])} 个版本(v{number} 查看)"))
138
+ index = result["index"]
139
+ lines.append(S.dim(f"共 {result['total']} 项 · 索引 {hhmm(index['built_at'])},{index['file_count']} 个文件"))
140
+ return lines
141
+
142
+
143
+ def render_versions(item: dict) -> list[str]:
144
+ lines = [S.bold(item["title"] or item["name"]),
145
+ f" {hhmm(item['modified'])} {item['path']} {S.green('最新')}"]
146
+ lines += [f" {hhmm(version['modified'])} {version['path']}" for version in item["versions"]]
147
+ return lines
148
+
149
+
150
+ def render_refs(result: dict) -> list[str]:
151
+ lines = [S.bold(result["path"])]
152
+ lines += [S.cyan(" 被引用:")] + [f" ← {path}" for path in result["inbound"]] if result["inbound"] \
153
+ else [S.dim(" 没有被其他资料或事项引用")]
154
+ if result["outbound"]:
155
+ lines += [S.cyan(" 引用了:")] + [f" → {path}" for path in result["outbound"]]
156
+ return lines
157
+
158
+
159
+ def render_items(result: dict, *, expand_closed: bool = False) -> tuple[list[str], list[dict]]:
160
+ """Grouped list; returns the lines and the numbered rows (closed groups only when expanded)."""
161
+ width = term_width()
162
+ lines, numbered = [], []
163
+ rows = result["items"]
164
+ for key, label in GROUPS:
165
+ members = [item for item in rows if item["group"] == key]
166
+ if not members:
167
+ continue
168
+ if key in CLOSED and not expand_closed:
169
+ lines.append(S.dim(f"{label}({len(members)}) 输入 all 展开"))
170
+ continue
171
+ lines.append(S.bold(f"{label}({len(members)})"))
172
+ for item in members:
173
+ numbered.append(item)
174
+ date = hhmm(item["updated_at"])
175
+ left = f" {S.bold(S.cyan(f'{len(numbered):>2}'))} {clip(item['title'], 26)}"
176
+ if item["waiting_for"]:
177
+ hint = f"等待:{item['waiting_for']}"
178
+ elif item["next_action"] and key not in CLOSED:
179
+ hint = f"下一步:{item['next_action']}"
180
+ else:
181
+ hint = (item["last_record"] or {}).get("text", "") or item["goal"]
182
+ line = pad(left, 36) + S.dim(clip(hint, width - 52))
183
+ lines.append(pad(line, width - len(date)) + S.dim(date))
184
+ if not rows:
185
+ lines.append(S.yellow("还没有事项。") + S.dim("可以在「记录事项」里新建第一个。"))
186
+ lines.append(S.dim(f"来源:{result['source']} · 读取于 {hhmm(result['read_at'])}"))
187
+ return lines, numbered
188
+
189
+
190
+ def render_item(item: dict) -> list[str]:
191
+ def row(label: str, value: str) -> str:
192
+ return f" {pad(label, 12)}{value or S.dim('—')}"
193
+
194
+ group = GROUP_LABEL.get(item["group"], item["status"])
195
+ status = item["status"] if item["status"] != item["group"] else group
196
+ last = item["last_record"]
197
+ lines = [f"{S.bold(item['title'])} [{status}]", row("编号", item["id"]), row("目标", item["goal"]),
198
+ row("最后记录", f"{hhmm(last['at'])} {last['text']}" if last else ""),
199
+ row("下一动作", item["next_action"]), row("等待", item["waiting_for"]),
200
+ row("恢复条件", item["resume_when"])]
201
+ if item["deliverables"]:
202
+ lines.append(row("交付物", f"d1 {item['deliverables'][0]}"))
203
+ lines += [f" {pad('', 12)}d{number} {path}" for number, path in enumerate(item["deliverables"][1:], 2)]
204
+ else:
205
+ lines.append(row("交付物", ""))
206
+ names = [*item["aliases"], *item["legacy_ids"]]
207
+ if names:
208
+ lines.append(row("别名/旧编号", "、".join(names)))
209
+ lines.append(row("权威记录", f"{item['path']} · 更新于 {hhmm(item['updated_at'])}"))
210
+ lines.append(S.dim(" 只显示已记录的内容,不推断优先级、重要性或是否完成。"))
211
+ return lines
212
+
213
+
214
+ def render_guide(guide: dict) -> list[str]:
215
+ lines = [S.bold("这个项目做什么"), f" {guide['about']}", "", S.bold("资料在哪")]
216
+ lines += [f" {pad(label, 18)}{path}" for label, path in guide["where"]]
217
+ lines += ["", S.bold("怎样开始")] + [f" {number}. {step}" for number, step in enumerate(guide["start"], 1)]
218
+ lines += ["", S.bold("常用操作")]
219
+ command = guide.get("command") or f"./{guide['launcher']}"
220
+ for goal, menu, cli in guide["examples"]:
221
+ lines.append(f" · {goal}:菜单 {menu};命令 {command} {cli}")
222
+ lines += ["", S.bold("现在能用的功能")]
223
+ for capability in guide["capabilities"]:
224
+ mark = S.green("可用") if capability["state"] == "available" else S.yellow("不可用")
225
+ offline = ",断网可用" if capability["offline"] else ",需要联网"
226
+ reason = f":{capability['reason']}" if capability["reason"] else ""
227
+ lines.append(f" {pad(capability['title'], 16)}{mark}{reason}{S.dim(offline)}")
228
+ lines += ["", S.dim(f"这份指南在本地生成,断网可读。随时运行 {command} 打开本菜单。")]
229
+ if guide.get("readme_mentions_entry") is False:
230
+ lines.append(S.yellow("README 还没有提到这个入口:在 README 写一句“运行 " + command + " 打开项目菜单”,新接手的人就能找到。"))
231
+ return lines
232
+
233
+
234
+ def render_health(health: dict) -> tuple[list[str], list[dict]]:
235
+ lines, numbered = [], []
236
+ source = health["update_source"]
237
+ if source["status"] == "online":
238
+ online = "联网检查:最新"
239
+ elif source["status"] == "cache":
240
+ online = f"联网检查:使用 {hhmm(source['cached_at'])} 的缓存(一天内)"
241
+ elif source["status"] == "unavailable":
242
+ online = S.yellow("联网检查:不可用(没有 agent-pack)")
243
+ elif source.get("cached_at"):
244
+ online = S.yellow(f"联网检查:当前离线,使用 {hhmm(source['cached_at'])} 的缓存")
245
+ else:
246
+ online = S.yellow("联网检查:当前离线,也没有缓存的版本信息")
247
+ lines.append(S.dim(f"读取于 {hhmm(health['read_at'])} · ") + online)
248
+ for key, label in LEVELS:
249
+ members = [finding for finding in health["findings"] if finding["level"] == key]
250
+ if not members:
251
+ continue
252
+ colour = S.red if key == "must" else S.yellow if key in ("suggest", "update") else S.dim
253
+ lines.append(colour(S.bold(f"{label}({len(members)})")))
254
+ for finding in members:
255
+ numbered.append(finding)
256
+ lines.append(f" {S.bold(S.cyan(f'{len(numbered):>2}'))} [{finding['area']}] {S.bold(finding['title'])}")
257
+ lines.append(f" 原因:{finding['reason']}")
258
+ lines.append(f" 影响:{finding['impact']}")
259
+ for step in finding["next"]:
260
+ lines.append(f" 下一步:{step['label']}" + S.dim(command_hint(step["action"])))
261
+ if health["ok"]:
262
+ lines.append(S.green("正常:") + "、".join(health["ok"]))
263
+ return lines, numbered
264
+
265
+
266
+ def command_hint(action: str) -> str:
267
+ """fix:rebuild-index -> (命令:fix rebuild-index); empty for advice-only steps."""
268
+ verb, _, target = action.partition(":")
269
+ return f"(命令:{verb} {target})" if verb in ("fix", "open") and target else ""
270
+
271
+
272
+ def render_plan(plan: dict, choices: dict[str, str] | None = None) -> list[str]:
273
+ lines = []
274
+ for number, step in enumerate(plan["steps"], 1):
275
+ lines.append(f" {S.bold(S.cyan(str(number)))} {S.bold(step['title'])}"
276
+ + (S.red(" 含不兼容变更") if step["breaking"] else ""))
277
+ lines += [f" 变化:{change}" for change in step["changes"]]
278
+ for write in step["writes"]:
279
+ lines.append(f" 将写入:{write['path']}({write['scope']})")
280
+ for item in step["custom"]:
281
+ chosen = (choices or {}).get(item["path"])
282
+ decision = chosen or item["default"]
283
+ if item["path"] == "README.md":
284
+ label = ("" if chosen else "默认") + ("不改" if decision == "keep" else "追加「怎样使用」")
285
+ else:
286
+ label = ("" if chosen else "默认") + ("保留你的版本" if decision == "keep" else "采用新模板")
287
+ lines.append(S.yellow(f" 定制文件:{item['path']}({item['state']})→ {label}"))
288
+ lines.append(f" 联网:{'需要' if step['network'] else '不需要'} · 恢复:{step['recovery']}")
289
+ if step["blocked"]:
290
+ lines.append(S.yellow(f" 暂不能执行:{step['blocked']}"))
291
+ if not plan["steps"]:
292
+ lines.append(S.green("没有需要处理的项。"))
293
+ return lines
294
+
295
+
296
+ def render_apply(result: dict) -> list[str]:
297
+ prefix = "【演示】" if result.get("simulated") else ""
298
+ lines = []
299
+ for item in result["results"]:
300
+ if item["status"] == "skipped":
301
+ lines.append(S.yellow(f" 跳过 {item['id']}:{item['detail']}"))
302
+ continue
303
+ lines.append(S.green(f" {prefix}已处理 {item['id']}"))
304
+ if item["written"]:
305
+ lines.append(f" 写入:{'、'.join(item['written'])}")
306
+ if item["kept"]:
307
+ lines.append(f" 保留:{'、'.join(item['kept'])}(未改动)")
308
+ if item["accepted"]:
309
+ lines.append(f" 采用新模板:{'、'.join(item['accepted'])}")
310
+ lines.append(f" 验证:{item['verify']}")
311
+ lines.append(f" 恢复位置:{item['recovery']}")
312
+ return lines
313
+
314
+
315
+ def render_preview(preview: dict) -> list[str]:
316
+ verb = "将新建" if preview.get("kind") == "new" or preview.get("target") is None else "将修改"
317
+ lines = [S.bold(f"{verb} {preview['path']}")]
318
+ for line in preview["lines"]:
319
+ lines.append(S.green(line) if line.startswith("+") else S.red(line) if line.startswith("-") else S.dim(line))
320
+ if preview["expect_updated_at"]:
321
+ lines.append(S.dim(f"写入前会核对 updated_at 仍为 {preview['expect_updated_at']},别人改过就不写入"))
322
+ return lines
323
+
324
+
325
+ def render_tools(tools: list[dict]) -> list[str]:
326
+ if not tools:
327
+ return [S.dim("这个项目没有声明专用工具。项目可在 .agents/moe.sakanano.agent-pack/tools.json 声明。")]
328
+ lines = []
329
+ for number, tool in enumerate(tools, 1):
330
+ mark = S.green("可用") if tool["state"] == "available" else S.yellow(f"不可用:{tool['reason']}")
331
+ lines.append(f" {S.bold(S.cyan(str(number)))} {S.bold(tool['title'])} {mark}")
332
+ if tool["description"]:
333
+ lines.append(f" {tool['description']}")
334
+ lines.append(S.dim(f" 命令:{' '.join(tool['command'])}"))
335
+ account = f" · 账号:{tool['account']}" if tool["account"] else ""
336
+ lines.append(S.dim(f" 联网:{'需要' if tool['network'] else '不需要'}{account} · 写入:{'、'.join(tool['writes']) or '无'}"))
337
+ return lines
338
+
339
+
340
+ def render_capabilities(capabilities: list[dict]) -> list[str]:
341
+ return [f" {pad(item['title'], 16)}{S.green('可用') if item['state'] == 'available' else S.yellow('不可用')}"
342
+ + (f":{item['reason']}" if item["reason"] else "") for item in capabilities]
343
+
344
+
345
+ def render_error(error: EntryError) -> list[str]:
346
+ lines = [S.red(f"出错:{error}")]
347
+ lines += [f" · {step}" for step in error.next_steps]
348
+ return lines
349
+
350
+
351
+ # ---------------------------------------------------------------- commands
352
+
353
+ def _act_target(result: dict, number: int) -> dict:
354
+ if not 1 <= number <= len(result["results"]):
355
+ raise EntryError(f"没有第 {number} 项(共 {len(result['results'])} 项)", "not-found")
356
+ return result["results"][number - 1]
357
+
358
+
359
+ def cmd_find(provider: Provider, args: argparse.Namespace) -> tuple[int, Any, list[str]]:
360
+ result = provider.find(args.terms, ext=args.type, container=args.container, limit=args.limit)
361
+ lines = header(provider, "找资料") + render_find(result)
362
+ for action in ("open", "reveal", "copy"):
363
+ number = getattr(args, action)
364
+ if number is not None:
365
+ done = provider.act(action, _act_target(result, number)["path"])
366
+ result = {**result, "action": done}
367
+ lines.append(done["message"])
368
+ return (EXIT_OK if result["results"] else EXIT_ATTENTION), result, lines
369
+
370
+
371
+ def cmd_open(provider: Provider, args: argparse.Namespace) -> tuple[int, Any, list[str]]:
372
+ action = "reveal" if args.reveal else "copy" if args.copy else "open"
373
+ done = provider.act(action, args.path)
374
+ return EXIT_OK, done, [done["message"]]
375
+
376
+
377
+ def cmd_refs(provider: Provider, args: argparse.Namespace) -> tuple[int, Any, list[str]]:
378
+ result = provider.refs(args.path)
379
+ return EXIT_OK, result, render_refs(result)
380
+
381
+
382
+ def cmd_progress(provider: Provider, args: argparse.Namespace) -> tuple[int, Any, list[str]]:
383
+ result = provider.items(status=args.status, term=args.term)
384
+ lines, _ = render_items(result, expand_closed=args.all or args.status in CLOSED)
385
+ return (EXIT_OK if result["items"] or not (args.term or args.status) else EXIT_ATTENTION), result, \
386
+ header(provider, "看进展") + lines
387
+
388
+
389
+ def cmd_show(provider: Provider, args: argparse.Namespace) -> tuple[int, Any, list[str]]:
390
+ item = provider.item(args.item)
391
+ return EXIT_OK, item, render_item(item)
392
+
393
+
394
+ def cmd_guide(provider: Provider, args: argparse.Namespace) -> tuple[int, Any, list[str]]:
395
+ guide = provider.guide()
396
+ return EXIT_OK, guide, header(provider, "项目指南") + render_guide(guide)
397
+
398
+
399
+ def cmd_health(provider: Provider, args: argparse.Namespace) -> tuple[int, Any, list[str]]:
400
+ health = provider.health()
401
+ lines, _ = render_health(health)
402
+ must = any(finding["level"] == "must" for finding in health["findings"])
403
+ return (EXIT_ATTENTION if must else EXIT_OK), health, header(provider, "检查项目与环境") + lines
404
+
405
+
406
+ def cmd_fix(provider: Provider, args: argparse.Namespace) -> tuple[int, Any, list[str]]:
407
+ choices = {path: "keep" for path in args.keep} | {path: "accept" for path in args.accept}
408
+ if args.entry_name:
409
+ choices["entry-name"] = args.entry_name
410
+ plan = provider.fix_plan(args.ids, choices)
411
+ lines = header(provider, "修复与更新") + render_plan(plan, choices)
412
+ if not args.apply:
413
+ lines.append(S.dim("以上为预览,没有写入。确认后加 --apply;定制文件用 --keep / --accept 指定。"))
414
+ return EXIT_OK, {"plan": plan, "applied": False}, lines
415
+ result = provider.apply_fix(plan, choices)
416
+ return EXIT_OK, {"plan": plan, "applied": True, **result}, lines + render_apply(result)
417
+
418
+
419
+ def _record_change(args: argparse.Namespace) -> dict:
420
+ status = "done" if args.close else "active" if args.reopen else args.status
421
+ return {"title": args.title, "goal": args.goal, "slug": args.slug, "log": args.log,
422
+ "next_action": args.next, "waiting_for": args.waiting, "status": status,
423
+ "add_deliverable": args.add_deliverable}
424
+
425
+
426
+ def cmd_record(provider: Provider, args: argparse.Namespace) -> tuple[int, Any, list[str]]:
427
+ target = None if args.item == "new" else args.item
428
+ if target is None and not (args.title and args.goal):
429
+ raise EntryError("新建事项需要 --title 与 --goal", "usage")
430
+ preview = provider.record_preview(target, _record_change(args))
431
+ # 预览与写入分两次运行时,用预览当时的值核对;同一次运行里预览本身就是核对基准。
432
+ for key in ("expect_updated_at", "expect_sha256"):
433
+ if getattr(args, key):
434
+ preview[key] = getattr(args, key)
435
+ lines = header(provider, "记录事项") + render_preview(preview)
436
+ if not args.apply:
437
+ lines.append(S.dim("以上为预览,没有写入。确认后加 --apply。"))
438
+ return EXIT_OK, {"preview": preview, "applied": False}, lines
439
+ result = provider.record_apply(preview)
440
+ return EXIT_OK, {"preview": preview, "applied": True, **result}, lines + [S.green(result["verify"])]
441
+
442
+
443
+ def cmd_tools(provider: Provider, args: argparse.Namespace) -> tuple[int, Any, list[str]]:
444
+ tools = provider.tools()
445
+ if args.tool is None:
446
+ return EXIT_OK, tools, header(provider, "项目工具") + render_tools(tools)
447
+ tool = next((tool for tool in tools if tool["id"] == args.tool), None)
448
+ if tool is None:
449
+ raise EntryError(f"项目没有声明这个工具:{args.tool}", "not-found",
450
+ ["已声明:" + ("、".join(tool["id"] for tool in tools) or "(无)")])
451
+ lines = render_tools([tool])
452
+ if not (args.run and args.apply):
453
+ lines.append(S.dim("以上为说明,没有执行。确认后加 --run --apply。"))
454
+ return EXIT_OK, tool, lines
455
+ result = provider.run_tool(args.tool)
456
+ return EXIT_OK, result, lines + [result["message"]]
457
+
458
+
459
+ def cmd_capabilities(provider: Provider, args: argparse.Namespace) -> tuple[int, Any, list[str]]:
460
+ capabilities = provider.capabilities()
461
+ return EXIT_OK, capabilities, header(provider, "能力状态") + render_capabilities(capabilities)
462
+
463
+
464
+ def cmd_scenarios(_provider: Any, args: argparse.Namespace) -> tuple[int, Any, list[str]]:
465
+ items = project_demo.describe()
466
+ lines = [f" --demo {pad(item['id'], 10)}{item['name']}({item['kind']})" for item in items]
467
+ lines.append(" --demo-fault " + " | ".join(project_demo.FAULTS))
468
+ return EXIT_OK, {"scenarios": items, "faults": list(project_demo.FAULTS)}, lines
469
+
470
+
471
+ def cmd_home(provider: Provider, args: argparse.Namespace) -> tuple[int, Any, list[str]]:
472
+ if args.output == "text" and not args.no_input and sys.stdin.isatty() and sys.stdout.isatty():
473
+ import project_menu
474
+ return project_menu.run(provider), None, []
475
+ lines = render_overview(provider) + ["", S.bold("可做的事")]
476
+ lines += render_capabilities(provider.capabilities())
477
+ lines.append(S.dim("在终端里直接运行可进入菜单;脚本中请用子命令,见 --help。"))
478
+ return EXIT_OK, provider.overview(), lines
479
+
480
+
481
+ COMMANDS: dict[str, Callable[[Any, argparse.Namespace], tuple[int, Any, list[str]]]] = {
482
+ "find": cmd_find, "open": cmd_open, "refs": cmd_refs, "progress": cmd_progress, "show": cmd_show,
483
+ "guide": cmd_guide, "health": cmd_health, "fix": cmd_fix, "record": cmd_record, "tools": cmd_tools,
484
+ "capabilities": cmd_capabilities, "scenarios": cmd_scenarios, "home": cmd_home,
485
+ }
486
+
487
+
488
+ def parser() -> argparse.ArgumentParser:
489
+ shared = argparse.ArgumentParser(add_help=False)
490
+ result =argparse.ArgumentParser(prog="agenthelm project", description="项目入口:不需要 AI Agent 的项目自助菜单",
491
+ formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__)
492
+
493
+ def globals_on(target: argparse.ArgumentParser, default: Any) -> None:
494
+ def value(normal: Any) -> Any:
495
+ return argparse.SUPPRESS if default is argparse.SUPPRESS else normal
496
+ target.add_argument("--project", type=Path, default=value(None), help="项目目录;缺省从当前目录向上查找")
497
+ target.add_argument("--output", choices=("text", "json"), default=value("text"))
498
+ target.add_argument("--no-input", action="store_true", default=value(False), help="不进入交互菜单")
499
+ target.add_argument("--demo", default=value(None), help="演示场景:study / personal / research / blank")
500
+ target.add_argument("--demo-fault", default=value(None), choices=project_demo.FAULTS)
501
+ target.add_argument("--offline", action="store_true", default=value(False),
502
+ help="不联网:版本检查只用缓存,需要联网的维护只预览")
503
+ target.add_argument("--via-launcher", type=int, default=value(None), help=argparse.SUPPRESS)
504
+
505
+ globals_on(result, None)
506
+ globals_on(shared, argparse.SUPPRESS)
507
+ sub = result.add_subparsers(dest="command")
508
+
509
+ def add(name: str, help_text: str) -> argparse.ArgumentParser:
510
+ return sub.add_parser(name, parents=[shared], help=help_text)
511
+
512
+ find = add("find", "找资料")
513
+ find.add_argument("terms", nargs="*")
514
+ find.add_argument("--type", help="文件类型,如 pdf、xlsx")
515
+ find.add_argument("--in", dest="container", help="限定事项或目录(片段)")
516
+ find.add_argument("--limit", type=int, default=10)
517
+ for action in ("open", "reveal", "copy"):
518
+ find.add_argument(f"--{action}", type=int, metavar="N", help=f"对第 N 项执行 {action}")
519
+ opener = add("open", "打开资料")
520
+ opener.add_argument("path")
521
+ opener.add_argument("--reveal", action="store_true")
522
+ opener.add_argument("--copy", action="store_true")
523
+ add("refs", "引用关系").add_argument("path")
524
+ progress = add("progress", "看进展")
525
+ progress.add_argument("term", nargs="?")
526
+ progress.add_argument("--status", choices=tuple(GROUP_LABEL))
527
+ progress.add_argument("--all", action="store_true", help="同时列出已完成与放弃的事项")
528
+ add("show", "事项详情").add_argument("item")
529
+ add("guide", "项目指南")
530
+ add("health", "检查项目与环境")
531
+ fix = add("fix", "修复与更新")
532
+ fix.add_argument("ids", nargs="*")
533
+ fix.add_argument("--apply", action="store_true")
534
+ fix.add_argument("--keep", action="append", default=[])
535
+ fix.add_argument("--accept", action="append", default=[])
536
+ fix.add_argument("--entry-name", help="add-entry:启动器文件名(缺省 menu)")
537
+ record = add("record", "记录事项")
538
+ record.add_argument("item", help="事项,或 new 新建")
539
+ for name in ("title", "goal", "slug", "log", "next", "waiting"):
540
+ record.add_argument(f"--{name}")
541
+ record.add_argument("--add-deliverable", action="append", default=[], metavar="路径",
542
+ help="登记资料或交付物(相对项目根目录,可重复)")
543
+ record.add_argument("--status", choices=tuple(GROUP_LABEL))
544
+ record.add_argument("--close", action="store_true")
545
+ record.add_argument("--reopen", action="store_true")
546
+ record.add_argument("--expect-updated-at", help="只在事项的 updated_at 仍为这个值时写入(取自预览的 JSON)")
547
+ record.add_argument("--expect-sha256", help="只在权威记录的内容摘要仍为这个值时写入(取自预览的 JSON)")
548
+ record.add_argument("--apply", action="store_true")
549
+ tools = add("tools", "项目工具")
550
+ tools.add_argument("tool", nargs="?")
551
+ tools.add_argument("--run", action="store_true")
552
+ tools.add_argument("--apply", action="store_true")
553
+ add("capabilities", "能力状态")
554
+ add("scenarios", "列出演示场景")
555
+ return result
556
+
557
+
558
+ def emit(output: str, code: int, command: str, data: Any, lines: list[str], demo: bool) -> None:
559
+ if output == "json":
560
+ print(json.dumps({"ok": code in (EXIT_OK, EXIT_ATTENTION), "attention": code == EXIT_ATTENTION,
561
+ "exit_code": code, "command": command, "demo": demo, "data": data}, ensure_ascii=False, indent=2))
562
+ elif lines:
563
+ print("\n".join(lines))
564
+
565
+
566
+ def main(argv: list[str] | None = None) -> int:
567
+ argv = list(sys.argv[1:] if argv is None else argv)
568
+ try:
569
+ args = parser().parse_args(argv)
570
+ except SystemExit as exc:
571
+ return EXIT_OK if exc.code in (0, None) else EXIT_USAGE
572
+ command = args.command or "home"
573
+ output = args.output
574
+ try:
575
+ if command == "scenarios":
576
+ code, data, lines = cmd_scenarios(None, args)
577
+ emit(output, code, command, data, lines, True)
578
+ return code
579
+ provider = open_provider(args)
580
+ check_entry_api(provider)
581
+ code, data, lines = COMMANDS[command](provider, args)
582
+ emit(output, code, command, data, lines, provider.simulated)
583
+ return code
584
+ except EntryError as exc:
585
+ code = EXIT_BY_CODE.get(exc.code, EXIT_USAGE)
586
+ if output == "json":
587
+ print(json.dumps({"ok": False, "exit_code": code, "command": command, "demo": bool(args.demo),
588
+ "error": {"code": exc.code, "message": str(exc), "next": exc.next_steps}},
589
+ ensure_ascii=False, indent=2))
590
+ else:
591
+ print("\n".join(render_error(EntryError(str(exc), exc.code, exc.next_steps))), file=sys.stderr)
592
+ return code
593
+ except KeyboardInterrupt:
594
+ print()
595
+ return EXIT_INTERRUPTED
596
+
597
+
598
+ if __name__ == "__main__":
599
+ raise SystemExit(main())