@sakanano-moe/agenthelm 6.7.0 → 6.11.0-rc.1

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 (86) hide show
  1. package/package.json +1 -1
  2. package/runtime/index.json +6 -6
  3. package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +1 -1
  4. package/runtime/plugins/agent-pack/hooks/hooks.json +32 -0
  5. package/runtime/plugins/agent-pack/plugin.json +1 -1
  6. package/runtime/plugins/agent-pack/references/local-only.md +126 -0
  7. package/runtime/plugins/agent-pack/references/nudge.en.md +58 -0
  8. package/runtime/plugins/agent-pack/references/nudge.md +58 -0
  9. package/runtime/plugins/agent-pack/runtime/agent_pack_nudge.py +573 -0
  10. package/runtime/plugins/agent-pack/runtime/agenthelm.py +419 -107
  11. package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +77 -53
  12. package/runtime/plugins/agent-pack/runtime/i18n_agent_pack.py +78 -0
  13. package/runtime/plugins/agent-pack/runtime/local_only.py +1130 -0
  14. package/runtime/plugins/agent-pack/runtime/locale/en.json +385 -0
  15. package/runtime/plugins/agent-pack/runtime/mcp_server.py +25 -0
  16. package/runtime/plugins/agent-pack/runtime/nudge_text.py +107 -0
  17. package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +9 -1
  18. package/runtime/plugins/agents-init/.codex-plugin/plugin.json +1 -1
  19. package/runtime/plugins/agents-init/content.json +17 -2
  20. package/runtime/plugins/agents-init/plugin.json +1 -1
  21. package/runtime/plugins/agents-init/skills/agents-init/AGENT.template.md +47 -0
  22. package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +39 -1
  23. package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +404 -70
  24. package/runtime/plugins/agents-init/skills/agents-init/scripts/adopt.py +596 -0
  25. package/runtime/plugins/agents-init/skills/agents-init/scripts/i18n_init.py +78 -0
  26. package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +245 -72
  27. package/runtime/plugins/agents-init/skills/agents-init/scripts/locale/en.json +210 -0
  28. package/runtime/plugins/agents-init/skills/agents-init/scripts/regions.py +99 -0
  29. package/runtime/plugins/agents-init/skills/agents-init/scripts/scaffold.py +1081 -0
  30. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_adopt.py +551 -0
  31. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_init_project.py +46 -2
  32. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_scaffold.py +555 -0
  33. package/runtime/plugins/agents-init/skills/agents-init/skeleton.json +66 -0
  34. package/runtime/plugins/agents-init/skills/agents-init/skill.json +1 -1
  35. package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +1 -1
  36. package/runtime/plugins/opinion-manager/plugin.json +1 -1
  37. package/runtime/plugins/opinion-manager/references/inbox.en.md +48 -0
  38. package/runtime/plugins/opinion-manager/references/inbox.md +48 -0
  39. package/runtime/plugins/opinion-manager/runtime/context.py +19 -18
  40. package/runtime/plugins/opinion-manager/runtime/i18n_opinion.py +78 -0
  41. package/runtime/plugins/opinion-manager/runtime/inbox.py +397 -0
  42. package/runtime/plugins/opinion-manager/runtime/inbox_text.py +101 -0
  43. package/runtime/plugins/opinion-manager/runtime/lifecycle.py +45 -44
  44. package/runtime/plugins/opinion-manager/runtime/locale/en.json +184 -0
  45. package/runtime/plugins/opinion-manager/runtime/opinion_manager.py +55 -50
  46. package/runtime/plugins/opinion-manager/runtime/versioned.py +84 -83
  47. package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +15 -2
  48. package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +1 -1
  49. package/runtime/plugins/project-orchestrator/content.json +30 -3
  50. package/runtime/plugins/project-orchestrator/plugin.json +1 -1
  51. package/runtime/plugins/project-orchestrator/references/case-workspaces.md +9 -1
  52. package/runtime/plugins/project-orchestrator/references/case.schema.json +1 -0
  53. package/runtime/plugins/project-orchestrator/references/sop-interview.md +83 -0
  54. package/runtime/plugins/project-orchestrator/references/sop.md +156 -0
  55. package/runtime/plugins/project-orchestrator/runtime/assets.py +52 -57
  56. package/runtime/plugins/project-orchestrator/runtime/case.py +139 -80
  57. package/runtime/plugins/project-orchestrator/runtime/case_structure.py +71 -0
  58. package/runtime/plugins/project-orchestrator/runtime/case_workspace.py +47 -46
  59. package/runtime/plugins/project-orchestrator/runtime/i18n_orchestrator.py +78 -0
  60. package/runtime/plugins/project-orchestrator/runtime/locale/en.json +1409 -0
  61. package/runtime/plugins/project-orchestrator/runtime/memory.py +44 -43
  62. package/runtime/plugins/project-orchestrator/runtime/private_paths.py +119 -0
  63. package/runtime/plugins/project-orchestrator/runtime/project_base.py +37 -19
  64. package/runtime/plugins/project-orchestrator/runtime/project_cli.py +267 -174
  65. package/runtime/plugins/project-orchestrator/runtime/project_demo.py +130 -48
  66. package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +524 -134
  67. package/runtime/plugins/project-orchestrator/runtime/project_menu.py +159 -85
  68. package/runtime/plugins/project-orchestrator/runtime/project_real.py +191 -106
  69. package/runtime/plugins/project-orchestrator/runtime/project_write.py +58 -55
  70. package/runtime/plugins/project-orchestrator/runtime/sop.py +1670 -0
  71. package/runtime/plugins/project-orchestrator/runtime/sop_structure.py +226 -0
  72. package/runtime/plugins/project-orchestrator/runtime/wf.py +74 -70
  73. package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +22 -0
  74. package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.en.md +57 -0
  75. package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.md +57 -0
  76. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.en.md +77 -0
  77. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.md +77 -0
  78. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.en.md +11 -0
  79. package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.md +11 -0
  80. package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.en.md +87 -0
  81. package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.md +87 -0
  82. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.en.md +75 -0
  83. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.md +75 -0
  84. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.en.md +11 -0
  85. package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.md +12 -0
  86. package/runtime/plugins/project-orchestrator/templates/CASE.en.md +39 -0
@@ -13,9 +13,11 @@ from pathlib import Path
13
13
  from typing import Any
14
14
 
15
15
  import context
16
+ import inbox
16
17
  import lifecycle
17
18
  import versioned
18
19
  from constants import KNOWN_PLACEHOLDERS
20
+ from i18n_opinion import _ # noqa: E402
19
21
 
20
22
 
21
23
  PLUGIN_ROOT = Path(__file__).resolve().parents[1]
@@ -33,37 +35,37 @@ class OpinionError(RuntimeError):
33
35
  def read_json_object(path: Path) -> dict[str, Any]:
34
36
  metadata = path.lstat()
35
37
  if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode):
36
- raise OpinionError(f"拒绝读取非普通文件:{path}")
38
+ raise OpinionError(_("拒绝读取非普通文件:{0}").format(path))
37
39
  value = json.loads(path.read_text(encoding="utf-8"))
38
40
  if not isinstance(value, dict):
39
- raise OpinionError(f"JSON 根节点必须是对象:{path}")
41
+ raise OpinionError(_("JSON 根节点必须是对象:{0}").format(path))
40
42
  return value
41
43
 
42
44
 
43
45
  def validate_rule(rule: Any, template_id: str) -> dict[str, Any]:
44
46
  if not isinstance(rule, dict):
45
- raise OpinionError(f"模板 {template_id} 的 rules 项必须是对象")
47
+ raise OpinionError(_("模板 {0} 的 rules 项必须是对象").format(template_id))
46
48
  required = {"id", "title", "level", "scopes", "text"}
47
49
  missing = required - set(rule)
48
50
  if missing:
49
- raise OpinionError(f"规则缺少字段 {sorted(missing)}:{template_id}")
51
+ raise OpinionError(_("规则缺少字段 {0}:{1}").format(sorted(missing), template_id))
50
52
  unknown = set(rule) - required
51
53
  if unknown:
52
- raise OpinionError(f"规则包含未知字段 {sorted(unknown)}:{template_id}")
54
+ raise OpinionError(_("规则包含未知字段 {0}:{1}").format(sorted(unknown), template_id))
53
55
  rule_id = rule["id"]
54
56
  if not isinstance(rule_id, str) or not ID_PATTERN.fullmatch(rule_id):
55
- raise OpinionError(f"规则编号无效:{rule_id!r}")
57
+ raise OpinionError(_("规则编号无效:{0!r}").format(rule_id))
56
58
  if rule["level"] not in {"required", "preferred"}:
57
- raise OpinionError(f"规则 level 无效:{rule_id}")
59
+ raise OpinionError(_("规则 level 无效:{0}").format(rule_id))
58
60
  if not isinstance(rule["title"], str) or not rule["title"].strip():
59
- raise OpinionError(f"规则标题为空:{rule_id}")
61
+ raise OpinionError(_("规则标题为空:{0}").format(rule_id))
60
62
  if not isinstance(rule["text"], str) or not rule["text"].strip():
61
- raise OpinionError(f"规则正文为空:{rule_id}")
63
+ raise OpinionError(_("规则正文为空:{0}").format(rule_id))
62
64
  scopes = rule["scopes"]
63
65
  if not isinstance(scopes, list) or not scopes or not all(
64
66
  isinstance(scope, str) and scope.strip() for scope in scopes
65
67
  ) or len(scopes) != len(set(scopes)):
66
- raise OpinionError(f"规则 scopes 必须是非空字符串数组:{rule_id}")
68
+ raise OpinionError(_("规则 scopes 必须是非空字符串数组:{0}").format(rule_id))
67
69
  return rule
68
70
 
69
71
 
@@ -71,77 +73,77 @@ def validate_template(value: dict[str, Any], path: Path) -> dict[str, Any]:
71
73
  required = {"id", "version", "name", "description", "scopes", "rules", "questions"}
72
74
  missing = required - set(value)
73
75
  if missing:
74
- raise OpinionError(f"模板缺少字段 {sorted(missing)}:{path}")
76
+ raise OpinionError(_("模板缺少字段 {0}:{1}").format(sorted(missing), path))
75
77
  unknown = set(value) - required
76
78
  if unknown:
77
- raise OpinionError(f"模板包含未知字段 {sorted(unknown)}:{path}")
79
+ raise OpinionError(_("模板包含未知字段 {0}:{1}").format(sorted(unknown), path))
78
80
  template_id = value["id"]
79
81
  if not isinstance(template_id, str) or not ID_PATTERN.fullmatch(template_id):
80
- raise OpinionError(f"模板编号无效:{template_id!r}")
82
+ raise OpinionError(_("模板编号无效:{0!r}").format(template_id))
81
83
  if path.stem != template_id:
82
- raise OpinionError(f"模板文件名必须与编号一致:{path.name}")
84
+ raise OpinionError(_("模板文件名必须与编号一致:{0}").format(path.name))
83
85
  if not isinstance(value["version"], str) or not VERSION_PATTERN.fullmatch(value["version"]):
84
- raise OpinionError(f"模板版本必须使用三段数字:{template_id}")
86
+ raise OpinionError(_("模板版本必须使用三段数字:{0}").format(template_id))
85
87
  for field in ("name", "description"):
86
88
  if not isinstance(value[field], str) or not value[field].strip():
87
- raise OpinionError(f"模板字段 {field} 不能为空:{template_id}")
89
+ raise OpinionError(_("模板字段 {0} 不能为空:{1}").format(field, template_id))
88
90
  if not isinstance(value["scopes"], list) or not value["scopes"] or not all(
89
91
  isinstance(scope, str) and scope.strip() for scope in value["scopes"]
90
92
  ) or len(value["scopes"]) != len(set(value["scopes"])):
91
- raise OpinionError(f"模板 scopes 必须是非空数组:{template_id}")
93
+ raise OpinionError(_("模板 scopes 必须是非空数组:{0}").format(template_id))
92
94
  rules = value["rules"]
93
95
  if not isinstance(rules, list) or not rules:
94
- raise OpinionError(f"模板 rules 必须是非空数组:{template_id}")
96
+ raise OpinionError(_("模板 rules 必须是非空数组:{0}").format(template_id))
95
97
  rule_ids: set[str] = set()
96
98
  for rule in rules:
97
99
  validated = validate_rule(rule, template_id)
98
100
  if validated["id"] in rule_ids:
99
- raise OpinionError(f"模板内存在重复规则编号:{validated['id']}")
101
+ raise OpinionError(_("模板内存在重复规则编号:{0}").format(validated['id']))
100
102
  rule_ids.add(validated["id"])
101
103
  questions = value["questions"]
102
104
  if not isinstance(questions, list):
103
- raise OpinionError(f"模板 questions 必须是数组:{template_id}")
105
+ raise OpinionError(_("模板 questions 必须是数组:{0}").format(template_id))
104
106
  question_ids: set[str] = set()
105
107
  for question in questions:
106
108
  if not isinstance(question, dict) or set(question) != {"id", "prompt", "options"}:
107
- raise OpinionError(f"模板问题结构无效:{template_id}")
109
+ raise OpinionError(_("模板问题结构无效:{0}").format(template_id))
108
110
  if not isinstance(question["id"], str) or not ID_PATTERN.fullmatch(question["id"]):
109
- raise OpinionError(f"模板问题编号无效:{template_id}")
111
+ raise OpinionError(_("模板问题编号无效:{0}").format(template_id))
110
112
  if question["id"] in question_ids:
111
- raise OpinionError(f"模板问题编号重复:{question['id']}")
113
+ raise OpinionError(_("模板问题编号重复:{0}").format(question['id']))
112
114
  question_ids.add(question["id"])
113
115
  if not isinstance(question["prompt"], str) or not question["prompt"].strip():
114
- raise OpinionError(f"模板问题正文为空:{template_id}")
116
+ raise OpinionError(_("模板问题正文为空:{0}").format(template_id))
115
117
  if not isinstance(question["options"], list) or len(question["options"]) < 2:
116
- raise OpinionError(f"模板问题至少需要两个选项:{template_id}")
118
+ raise OpinionError(_("模板问题至少需要两个选项:{0}").format(template_id))
117
119
  for option in question["options"]:
118
120
  if not isinstance(option, dict) or set(option) != {"label", "recommend"}:
119
- raise OpinionError(f"模板问题选项结构无效:{template_id}")
121
+ raise OpinionError(_("模板问题选项结构无效:{0}").format(template_id))
120
122
  if not isinstance(option["label"], str) or not option["label"].strip():
121
- raise OpinionError(f"模板问题选项标签为空:{template_id}")
123
+ raise OpinionError(_("模板问题选项标签为空:{0}").format(template_id))
122
124
  recommendations = option["recommend"]
123
125
  if not isinstance(recommendations, list) or not recommendations or not all(
124
126
  isinstance(recommendation, str) and recommendation in rule_ids
125
127
  for recommendation in recommendations
126
128
  ) or len(recommendations) != len(set(recommendations)):
127
- raise OpinionError(f"模板问题引用了未知规则:{template_id}")
129
+ raise OpinionError(_("模板问题引用了未知规则:{0}").format(template_id))
128
130
  return value
129
131
 
130
132
 
131
133
  def load_catalog() -> OrderedDict[str, dict[str, Any]]:
132
134
  if not TEMPLATE_ROOT.is_dir() or TEMPLATE_ROOT.is_symlink():
133
- raise OpinionError(f"模板目录不可用:{TEMPLATE_ROOT}")
135
+ raise OpinionError(_("模板目录不可用:{0}").format(TEMPLATE_ROOT))
134
136
  catalog: OrderedDict[str, dict[str, Any]] = OrderedDict()
135
137
  global_rules: dict[str, dict[str, Any]] = {}
136
138
  for path in sorted(TEMPLATE_ROOT.glob("*.json")):
137
139
  template = validate_template(read_json_object(path), path)
138
140
  template_id = template["id"]
139
141
  if template_id in catalog:
140
- raise OpinionError(f"模板编号重复:{template_id}")
142
+ raise OpinionError(_("模板编号重复:{0}").format(template_id))
141
143
  for rule in template["rules"]:
142
144
  current = global_rules.get(rule["id"])
143
145
  if current is not None and current != rule:
144
- raise OpinionError(f"相同规则编号的内容不一致:{rule['id']}")
146
+ raise OpinionError(_("相同规则编号的内容不一致:{0}").format(rule['id']))
145
147
  global_rules[rule["id"]] = rule
146
148
  catalog[template_id] = template
147
149
  return catalog
@@ -150,10 +152,10 @@ def load_catalog() -> OrderedDict[str, dict[str, Any]]:
150
152
  def parse_template_spec(spec: str, catalog: OrderedDict[str, dict[str, Any]]) -> str:
151
153
  template_id, separator, version = spec.partition("@")
152
154
  if template_id not in catalog:
153
- raise OpinionError(f"未知模板:{template_id}")
155
+ raise OpinionError(_("未知模板:{0}").format(template_id))
154
156
  current = catalog[template_id]["version"]
155
157
  if separator and version != current:
156
- raise OpinionError(f"模板版本不匹配:{template_id}@{version},当前版本为 {current}")
158
+ raise OpinionError(_("模板版本不匹配:{0}@{1},当前版本为 {2}").format(template_id, version, current))
157
159
  return template_id
158
160
 
159
161
 
@@ -170,10 +172,10 @@ def read_custom_file(path: Path | None) -> str | None:
170
172
  return None
171
173
  metadata = path.lstat()
172
174
  if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode):
173
- raise OpinionError(f"自定义规则必须来自普通文件:{path}")
175
+ raise OpinionError(_("自定义规则必须来自普通文件:{0}").format(path))
174
176
  content = path.read_text(encoding="utf-8").strip()
175
177
  if not content:
176
- raise OpinionError("自定义规则文件为空")
178
+ raise OpinionError(_("自定义规则文件为空"))
177
179
  return content
178
180
 
179
181
 
@@ -197,7 +199,7 @@ def select_rules(
197
199
  indexed = rule_index(catalog)
198
200
  for rule_id in rule_ids:
199
201
  if rule_id not in indexed:
200
- raise OpinionError(f"未知规则:{rule_id}")
202
+ raise OpinionError(_("未知规则:{0}").format(rule_id))
201
203
  template_id, rule = indexed[rule_id]
202
204
  source_versions[template_id] = catalog[template_id]["version"]
203
205
  if rule_id not in selected_ids:
@@ -292,28 +294,28 @@ def write_atomic(path: Path, content: str) -> None:
292
294
  def apply_opinion(project: Path, content: str, *, replace: bool) -> str:
293
295
  root = project.expanduser().resolve()
294
296
  if not root.is_dir():
295
- raise OpinionError(f"项目目录不存在:{root}")
297
+ raise OpinionError(_("项目目录不存在:{0}").format(root))
296
298
  destination = root / OPINION_FILE
297
299
  if destination.is_symlink():
298
- raise OpinionError(f"拒绝写入符号链接:{destination}")
300
+ raise OpinionError(_("拒绝写入符号链接:{0}").format(destination))
299
301
  if destination.exists():
300
302
  if not destination.is_file():
301
- raise OpinionError(f"目标路径不是普通文件:{destination}")
303
+ raise OpinionError(_("目标路径不是普通文件:{0}").format(destination))
302
304
  current = destination.read_text(encoding="utf-8")
303
305
  if current == content:
304
306
  return "unchanged"
305
307
  if not replace and not known_placeholder(current):
306
- raise OpinionError("OPINION.md 已包含内容;预览差异并明确使用 --replace")
308
+ raise OpinionError(_("OPINION.md 已包含内容;预览差异并明确使用 --replace"))
307
309
  write_atomic(destination, content)
308
310
  if destination.read_text(encoding="utf-8") != content:
309
- raise OpinionError("写入后核对 OPINION.md 失败")
311
+ raise OpinionError(_("写入后核对 OPINION.md 失败"))
310
312
  return "written"
311
313
 
312
314
 
313
315
  def catalog_markdown(catalog: OrderedDict[str, dict[str, Any]]) -> str:
314
- lines = ["# Opinion 模板目录", ""]
316
+ lines = [_("# Opinion 模板目录"), ""]
315
317
  if not catalog:
316
- lines.append("当前没有已批准的公开模板。")
318
+ lines.append(_("当前没有已批准的公开模板。"))
317
319
  return "\n".join(lines).rstrip() + "\n"
318
320
  for template in catalog.values():
319
321
  identity = f"{template['family']}/{template['variant']}" if "family" in template else template["id"]
@@ -322,7 +324,7 @@ def catalog_markdown(catalog: OrderedDict[str, dict[str, Any]]) -> str:
322
324
  "",
323
325
  template["description"],
324
326
  "",
325
- f"规则数量:{len(template['rules'])};适用场景:{'、'.join(template['scopes'])}。",
327
+ _("规则数量:{0};适用场景:{1}。").format(len(template['rules']), _('、').join(template['scopes'])),
326
328
  "",
327
329
  ))
328
330
  return "\n".join(lines).rstrip() + "\n"
@@ -336,13 +338,13 @@ def catalog_payload(catalog: OrderedDict[str, dict[str, Any]]) -> dict[str, Any]
336
338
 
337
339
 
338
340
  def build_parser() -> argparse.ArgumentParser:
339
- parser = argparse.ArgumentParser(description="管理版本化 Opinion 模板并生成统一 OPINION.md")
341
+ parser = argparse.ArgumentParser(description=_("管理版本化 Opinion 模板并生成统一 OPINION.md"))
340
342
  subparsers = parser.add_subparsers(dest="command", required=True)
341
343
 
342
- catalog = subparsers.add_parser("catalog", help="查看模板、规则和引导问题")
344
+ catalog = subparsers.add_parser("catalog", help=_("查看模板、规则和引导问题"))
343
345
  catalog.add_argument("--output", choices=("json", "markdown"), default="markdown")
344
346
 
345
- compose = subparsers.add_parser("compose", help="组合模板、规则和自定义内容")
347
+ compose = subparsers.add_parser("compose", help=_("组合模板、规则和自定义内容"))
346
348
  compose.add_argument("--project", type=Path, required=True)
347
349
  compose.add_argument("--template", action="append", default=[])
348
350
  compose.add_argument("--rule", action="append", default=[])
@@ -353,6 +355,7 @@ def build_parser() -> argparse.ArgumentParser:
353
355
  versioned.add_commands(subparsers, compose, catalog)
354
356
  context.add_command(subparsers)
355
357
  lifecycle.add_command(subparsers)
358
+ inbox.add_command(subparsers)
356
359
  return parser
357
360
 
358
361
 
@@ -361,6 +364,8 @@ def execute(args: argparse.Namespace) -> tuple[int, str]:
361
364
  return context.execute(args)
362
365
  if args.command == "lifecycle":
363
366
  return lifecycle.execute(args)
367
+ if args.command == "inbox":
368
+ return inbox.execute(args)
364
369
  if versioned.handles(args):
365
370
  return versioned.execute(args)
366
371
  catalog = load_catalog()
@@ -376,11 +381,11 @@ def execute(args: argparse.Namespace) -> tuple[int, str]:
376
381
 
377
382
  lock_path = args.project.expanduser() / "opinion.lock.json"
378
383
  if lock_path.exists() or lock_path.is_symlink():
379
- raise OpinionError("项目已经使用版本锁定;请用 compose --profile 更新")
384
+ raise OpinionError(_("项目已经使用版本锁定;请用 compose --profile 更新"))
380
385
 
381
386
  custom = read_custom_file(args.custom_file)
382
387
  if not args.template and not args.rule and custom is None:
383
- raise OpinionError("至少选择一个模板、一个规则或一份自定义规则文件")
388
+ raise OpinionError(_("至少选择一个模板、一个规则或一份自定义规则文件"))
384
389
  selected, sources = select_rules(catalog, args.template, args.rule)
385
390
  mode = selection_mode(args.template, args.rule, custom)
386
391
  content, metadata = render_opinion(catalog, selected, sources, custom, mode)
@@ -409,7 +414,7 @@ def main(argv: list[str] | None = None) -> int:
409
414
  if getattr(args, "output", "markdown") == "json":
410
415
  output = json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False, indent=2) + "\n"
411
416
  else:
412
- output = f"错误:{exc}\n"
417
+ output = _("错误:{0}\n").format(exc)
413
418
  code = 2
414
419
  sys.stdout.write(output)
415
420
  return code