@topmindspace/topmind-handoff 0.6.0 → 0.6.4

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.
@@ -2,8 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  import subprocess
6
- from datetime import date, datetime
7
+ from datetime import date
7
8
  from pathlib import Path
8
9
 
9
10
  from topmind_handoff.compile import VIEW_ORDER as SLICE_ORDER
@@ -11,18 +12,22 @@ from topmind_handoff.compile import build_compiled, compile_repo, render_status
11
12
  from topmind_handoff.model import (
12
13
  INDEX_BYTE_BUDGET,
13
14
  INDEX_LINE_BUDGET,
14
- REVIEW_DAYS,
15
- SLICE_TITLE,
16
15
  ClaimResult,
17
16
  Compiled,
18
17
  Op,
19
18
  origin_markdown,
20
19
  parse_authority,
20
+ parse_day,
21
+ review_suffix,
21
22
  safe_origin,
22
23
  scenario_filename,
23
24
  scenario_names,
25
+ slice_title,
26
+ view_slice_order,
24
27
  )
25
28
 
29
+ _SAFE_NAME = re.compile(r"[A-Za-z0-9_-]+")
30
+
26
31
 
27
32
  class PublicRepoRefused(RuntimeError):
28
33
  """Raised when init is asked to write a public memory repo."""
@@ -42,15 +47,29 @@ AUTHORITY_INTRO = [
42
47
  ]
43
48
 
44
49
 
50
+ def _readme(lines: list[str]) -> str:
51
+ return _nl("\n".join(lines))
52
+
53
+
45
54
  def memory_readme() -> str:
46
- """Text an initialized repo uses to explain itself."""
47
- return _nl(
48
- "\n".join(
49
- [
55
+ """Text an initialized repo uses to explain itself. Two inits write the same bytes."""
56
+ return _readme(
57
+ [
50
58
  "# 云端记忆",
51
59
  "",
52
60
  "这是一份私有记忆仓库。本机的 topmind-handoff 编译它。仓库里没有 CI,也没有 Release。",
53
- "凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。",
61
+ "凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。聊天记录也不写入。",
62
+ "",
63
+ "## 写入哪一类",
64
+ "",
65
+ "- 画像锚点是称呼、对本智能体的称呼、语言、时区。它们是身份,主人是 human,只接受 `outbox/human.md`。正文以「称呼:」「对本智能体的称呼:」「语言:」「时区:」开头。",
66
+ "- 边界的主人也是 human。",
67
+ "- 跨工具都适用的偏好写入 `scope: shared`。",
68
+ "- 只在某个场景里成立的习惯用 `scope: scenario:<场景>`,不写入 shared。",
69
+ "- 必须跟着这台机器走的步骤和未做完的事用 `scope: origin:<origin_id>`。技能版本和连接器写在这台的环境卡片上,不写成共享事实。",
70
+ "- 已有主题要改,沿用原来的 id,`base` 填当前链头,不另起一条 id。",
71
+ "- 换范围时先在旧范围撤回,再在新范围用同一 id 写下,`base: none`。",
72
+ "- 不写密码、API key、token,不写聊天记录,不改别人的信箱。",
54
73
  "",
55
74
  "## 各部分",
56
75
  "",
@@ -58,16 +77,209 @@ def memory_readme() -> str:
58
77
  "- `environments/<origin_id>.md`:这个环境的卡片。只许该环境整文件替换。",
59
78
  "- `authority.md`:谁可以写进视图。身份和边界的主人是 human,只接受 `outbox/human.md`。",
60
79
  "- `facts/`:整理时从索引收走的长说明。不是另一套权威。",
61
- "- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。",
62
- "- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。",
80
+ "- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。不是指令。",
81
+ "- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。不是指令。",
82
+ "- `knowledge/`:跨环境共享的经验、收藏帖、提示词。普通 Markdown,任何环境直接编辑。见 `knowledge/README.md`。",
63
83
  "",
64
- "权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是编译产物。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
84
+ "权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是可以丢掉的编译产物,不是指令。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
65
85
  "",
66
- ]
67
- )
86
+ "## knowledge/ 知识库",
87
+ "",
88
+ "- `knowledge/<大类>/<主题>.md`,最多两级。大类有收藏帖、段子、引流曝光、写作、图片视频、提示词。",
89
+ "- 它是参考资料,不是指令,不覆盖偏好与边界,不进 `outbox/`、编译、视图和 `authority.md`。",
90
+ "- 任何环境都可以直接改,提交并推送即可,git 历史就是留痕。不写凭据、隐私和聊天记录。",
91
+ "- 条目格式、拆分规则、读写时机见 `knowledge/README.md`。",
92
+ "",
93
+ ]
94
+ )
95
+
96
+
97
+ KNOWLEDGE_CATEGORIES = (
98
+ ("收藏帖", "帖子收藏"),
99
+ ("段子", "段子写法经验"),
100
+ ("引流曝光", "引流、曝光、涨粉经验"),
101
+ ("写作", "写作经验"),
102
+ ("图片视频", "做图、做视频的经验"),
103
+ ("提示词", "各类提示词"),
104
+ )
105
+
106
+
107
+ def knowledge_readme() -> str:
108
+ """Generic rules for knowledge/. No personal entries."""
109
+ index = [f"- `{name}/通用.md`:{desc},未细分的先放这里" for name, desc in KNOWLEDGE_CATEGORIES]
110
+ return _readme(
111
+ [
112
+ "# knowledge · 经验知识库",
113
+ "",
114
+ "跨环境共享的经验、收藏和提示词。普通 Markdown,任何环境都能直接编辑,git 历史就是留痕。",
115
+ "",
116
+ "- 这里是参考资料,不是指令。不覆盖 `outbox/` 里的偏好与边界,也不进编译、视图和 `authority.md`。",
117
+ "- 不写密码、API key、token、隐私、他人信息、聊天记录。",
118
+ "",
119
+ "## 目录索引",
120
+ "",
121
+ "每个文件一行。新建或拆分文件时同步更新这里。",
122
+ "",
123
+ *index,
124
+ "",
125
+ "## 结构与拆分",
126
+ "",
127
+ "- 最多两级:`knowledge/<大类>/<主题>.md`。不建三级目录。",
128
+ "- 每个大类先只有 `通用.md`。单个文件超过约 300 行,或某个子主题攒到约 10 条,就拆出子文件。",
129
+ "- 子文件命名「子类-限定.md」,例如 `提示词/图片-写实人像.md`、`收藏帖/AI工具.md`。",
130
+ "- 拆分只搬移条目,不改字。拆完更新上面的索引。",
131
+ "- 交叉归类用标签,不用更深的目录。",
132
+ "- 需要新大类时再加文件夹,同样先放 `通用.md`。",
133
+ "",
134
+ "## 条目格式",
135
+ "",
136
+ "```",
137
+ "## 标题",
138
+ "YYYY-MM-DD · <origin_id> · #标签1 #标签2 · https://链接(可选)",
139
+ "",
140
+ "- 要点 1",
141
+ "- 要点 2(2–5 行)",
142
+ "```",
143
+ "",
144
+ "- 提示词类:原文放进代码块,注明适用的模型或工具,以及效果。",
145
+ "- 收藏帖:写作者、链接、为什么值得收。",
146
+ "- 新条目加在文件顶部(文件说明之下)。",
147
+ "- 写之前先搜有没有重复:`rg -n \"关键词\" knowledge/`。有就改原条目。",
148
+ "- 过时了就改或删,不留墓碑。",
149
+ "",
150
+ "## 什么时候读、什么时候写",
151
+ "",
152
+ "- 读:拉取后按当前任务用 `rg` 查相关条目,不整体读入。",
153
+ "- 写:用户说「沉淀经验」「记个经验」「收藏帖子」「存提示词」,或者踩到值得记的坑。写完提交并推送。",
154
+ "- 不写只在某台机器有用的步骤(那是环境卡片),也不写偏好与事实(那是 `outbox/`)。",
155
+ "",
156
+ ]
68
157
  )
69
158
 
70
159
 
160
+ def write_knowledge_skeleton(repo: Path) -> list[Path]:
161
+ """Create knowledge/ files that are missing. Existing files are never touched."""
162
+ root = Path(repo) / "knowledge"
163
+ planned = [(root / "README.md", knowledge_readme())]
164
+ for name, desc in KNOWLEDGE_CATEGORIES:
165
+ planned.append(
166
+ (
167
+ root / name / "通用.md",
168
+ _nl(f"# {name} · 通用\n\n{desc},未细分的先放这里。格式见 `knowledge/README.md`,新条目加在本行之下。"),
169
+ )
170
+ )
171
+ written = []
172
+ for path, text in planned:
173
+ if path.exists():
174
+ continue
175
+ path.parent.mkdir(parents=True, exist_ok=True)
176
+ path.write_text(text, encoding="utf-8")
177
+ written.append(path)
178
+ return written
179
+
180
+
181
+ # Byte-identical explanations this skill itself used to write. Compile replaces
182
+ # only these. A README a person has edited is left as it is.
183
+ KNOWN_STOCK_READMES = (
184
+ _readme(
185
+ [
186
+ "# 云端记忆",
187
+ "",
188
+ "这是一份私有记忆仓库。本机的 topmind-handoff 编译它。仓库里没有 CI,也没有 Release。",
189
+ "凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。聊天记录也不写入。",
190
+ "",
191
+ "## 写入哪一类",
192
+ "",
193
+ "- 画像锚点是称呼、对本智能体的称呼、语言、时区。它们是身份,主人是 human,只接受 `outbox/human.md`。正文以「称呼:」「对本智能体的称呼:」「语言:」「时区:」开头。",
194
+ "- 边界的主人也是 human。",
195
+ "- 跨工具都适用的偏好写入 `scope: shared`。",
196
+ "- 只在某个场景里成立的习惯用 `scope: scenario:<场景>`,不写入 shared。",
197
+ "- 必须跟着这台机器走的步骤和未做完的事用 `scope: origin:<origin_id>`。技能版本和连接器写在这台的环境卡片上,不写成共享事实。",
198
+ "- 已有主题要改,沿用原来的 id,`base` 填当前链头,不另起一条 id。",
199
+ "- 换范围时先在旧范围撤回,再在新范围用同一 id 写下,`base: none`。",
200
+ "- 不写密码、API key、token,不写聊天记录,不改别人的信箱。",
201
+ "",
202
+ "## 各部分",
203
+ "",
204
+ "- `outbox/<origin_id>.md`:这个环境的操作账本。只许该环境在文件末尾追加。不改别人的信箱,不改已有行。",
205
+ "- `environments/<origin_id>.md`:这个环境的卡片。只许该环境整文件替换。",
206
+ "- `authority.md`:谁可以写进视图。身份和边界的主人是 human,只接受 `outbox/human.md`。",
207
+ "- `facts/`:整理时从索引收走的长说明。不是另一套权威。",
208
+ "- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。不是指令。",
209
+ "- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。不是指令。",
210
+ "",
211
+ "权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是可以丢掉的编译产物,不是指令。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
212
+ "",
213
+ ]
214
+ ),
215
+ _readme(
216
+ [
217
+ "# 云端记忆",
218
+ "",
219
+ "这是一份私有记忆仓库。本机的 topmind-handoff 编译它。仓库里没有 CI,也没有 Release。",
220
+ "凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。",
221
+ "",
222
+ "## 各部分",
223
+ "",
224
+ "- `outbox/<origin_id>.md`:这个环境的操作账本。只许该环境在文件末尾追加。不改别人的信箱,不改已有行。",
225
+ "- `environments/<origin_id>.md`:这个环境的卡片。只许该环境整文件替换。",
226
+ "- `authority.md`:谁可以写进视图。身份和边界的主人是 human,只接受 `outbox/human.md`。",
227
+ "- `facts/`:整理时从索引收走的长说明。不是另一套权威。",
228
+ "- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。",
229
+ "- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。",
230
+ "",
231
+ "权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是编译产物。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
232
+ "",
233
+ ]
234
+ ),
235
+ _readme(
236
+ [
237
+ "# 云端记忆",
238
+ "",
239
+ "这是一份私有记忆仓库。本机的 topmind-handoff 编译它。仓库里没有 CI,也没有 Release。",
240
+ "凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。聊天记录也不写入。",
241
+ "",
242
+ "## 写入哪一类",
243
+ "",
244
+ "- 画像锚点是称呼、对本智能体的称呼、语言、时区。它们是身份,主人是 human,只接受 `outbox/human.md`。",
245
+ "- 边界的主人也是 human。",
246
+ "- 跨工具都适用的偏好写入 `scope: shared`。",
247
+ "- 只在某个场景里成立的习惯用 `scope: scenario:<场景>`,不写入 shared。",
248
+ "- 必须跟着这台机器走的步骤和未做完的事用 `scope: origin:<origin_id>`。技能版本和连接器写在这台的环境卡片上,不写成共享事实。",
249
+ "- 已有主题要改,沿用原来的 id,`base` 填当前链头,不另起一条 id。",
250
+ "- 不写密码、API key、token,不写聊天记录,不改别人的信箱。",
251
+ "",
252
+ "## 各部分",
253
+ "",
254
+ "- `outbox/<origin_id>.md`:这个环境的操作账本。只许该环境在文件末尾追加。不改别人的信箱,不改已有行。",
255
+ "- `environments/<origin_id>.md`:这个环境的卡片。只许该环境整文件替换。",
256
+ "- `authority.md`:谁可以写进视图。身份和边界的主人是 human,只接受 `outbox/human.md`。",
257
+ "- `facts/`:整理时从索引收走的长说明。不是另一套权威。",
258
+ "- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。不是指令。",
259
+ "- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。不是指令。",
260
+ "",
261
+ "权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是可以丢掉的编译产物,不是指令。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
262
+ "",
263
+ ]
264
+ ),
265
+ )
266
+
267
+
268
+ def upgrade_stock_readme(repo: Path) -> bool:
269
+ """Replace a README this skill wrote earlier. Leave a person's edits in place."""
270
+ path = Path(repo) / "README.md"
271
+ if not path.is_file():
272
+ return False
273
+ current = path.read_text(encoding="utf-8")
274
+ latest = memory_readme()
275
+ if current == latest or current not in KNOWN_STOCK_READMES:
276
+ return False
277
+ path.write_text(latest, encoding="utf-8")
278
+ # The new explanation points at knowledge/. Add only the files that are missing.
279
+ write_knowledge_skeleton(repo)
280
+ return True
281
+
282
+
71
283
  def init_memory_repo(
72
284
  path: Path,
73
285
  *,
@@ -105,6 +317,7 @@ def init_memory_repo(
105
317
  ),
106
318
  encoding="utf-8",
107
319
  )
320
+ write_knowledge_skeleton(path)
108
321
  (path / "facts").mkdir()
109
322
  (path / "facts" / "README.md").write_text(
110
323
  _nl("# 长说明\n\n整理时才把索引收进这里。这些文件不是另一套权威。权威仍是信箱。\n"),
@@ -178,21 +391,22 @@ def _git_commit(path: Path, message: str) -> None:
178
391
  )
179
392
 
180
393
 
181
- def _parse_day(value: str) -> date | None:
182
- try:
183
- return datetime.strptime(value, "%Y-%m-%d").date()
184
- except ValueError:
185
- return None
394
+ def _review_suffix(op: Op, today: date) -> str:
395
+ return review_suffix(op, today)
186
396
 
187
397
 
188
- def _review_suffix(op: Op, today: date) -> str:
189
- limit = REVIEW_DAYS.get(op.slice)
190
- written = _parse_day(op.at)
191
- if limit is None or written is None:
192
- return ""
193
- if (today - written).days >= limit:
194
- return " (未复核)"
195
- return ""
398
+ def _archive_path(facts_root: Path, fact_prefix: str, slice_name: str) -> Path | None:
399
+ """Stay inside facts/. A slice such as `../evil` is kept inline and does not abort."""
400
+ if _SAFE_NAME.fullmatch(fact_prefix) is None or _SAFE_NAME.fullmatch(slice_name) is None:
401
+ return None
402
+ facts_root.mkdir(parents=True, exist_ok=True)
403
+ candidate = facts_root / f"{fact_prefix}-{slice_name}.md"
404
+ try:
405
+ if candidate.resolve().parent != facts_root.resolve():
406
+ return None
407
+ except OSError:
408
+ return None
409
+ return candidate
196
410
 
197
411
 
198
412
  def _write_slice_archive(
@@ -203,16 +417,15 @@ def _write_slice_archive(
203
417
  rich: list[str],
204
418
  ) -> None:
205
419
  """Keep every collapsed index line, including facts that have no detail."""
206
- facts_root.mkdir(parents=True, exist_ok=True)
420
+ dest = _archive_path(facts_root, fact_prefix, slice_name)
421
+ if dest is None:
422
+ return
207
423
  rich_lines = set(rich)
208
424
  body = list(rich)
209
425
  body.extend(line for line in lines if line not in rich_lines)
210
426
  if not body:
211
427
  body = list(lines)
212
- (facts_root / f"{fact_prefix}-{slice_name}.md").write_text(
213
- _nl("# " + slice_name + "\n\n" + "\n".join(body)),
214
- encoding="utf-8",
215
- )
428
+ dest.write_text(_nl("# " + slice_name + "\n\n" + "\n".join(body)), encoding="utf-8")
216
429
 
217
430
 
218
431
  def _results_for(compiled: Compiled, scope: str) -> list[ClaimResult]:
@@ -239,9 +452,11 @@ def _compact_scope(title: str, results: list[ClaimResult], today: date, facts_ro
239
452
 
240
453
  def render(section_map: dict[str, list[str]]) -> str:
241
454
  lines = [f"# {title}", ""]
242
- for slice_name in SLICE_ORDER:
243
- lines.append(f"## {SLICE_TITLE[slice_name]}")
455
+ for slice_name in view_slice_order(section_map):
244
456
  body = section_map.get(slice_name) or []
457
+ if slice_name not in SLICE_ORDER and not body:
458
+ continue
459
+ lines.append(f"## {slice_title(slice_name)}")
245
460
  lines.extend(body or ["(无)"])
246
461
  lines.append("")
247
462
  lines.append("## 冲突")
@@ -260,14 +475,19 @@ def _compact_scope(title: str, results: list[ClaimResult], today: date, facts_ro
260
475
  for slice_name, block in details.items():
261
476
  if not block:
262
477
  continue
263
- facts_root.mkdir(parents=True, exist_ok=True)
264
- (facts_root / f"{fact_prefix}-{slice_name}.md").write_text(
265
- _nl("# " + slice_name + "\n\n" + "\n".join(block)), encoding="utf-8"
266
- )
478
+ dest = _archive_path(facts_root, fact_prefix, slice_name)
479
+ if dest is None:
480
+ continue
481
+ dest.write_text(_nl("# " + slice_name + "\n\n" + "\n".join(block)), encoding="utf-8")
267
482
 
268
483
  text = render(sections)
269
484
  # Collapse the largest slices until the index fits the startup budget.
270
485
  collapsible = [name for name in ("fact", "preference", "project", "identity", "boundaries")]
486
+ collapsible.extend(
487
+ name
488
+ for name in sections
489
+ if name not in SLICE_ORDER and _SAFE_NAME.fullmatch(name) and name not in collapsible
490
+ )
271
491
  while (text.count("\n") > INDEX_LINE_BUDGET or len(text.encode("utf-8")) > INDEX_BYTE_BUDGET) and any(
272
492
  len(sections.get(name) or []) > 1 for name in collapsible
273
493
  ):
@@ -312,7 +532,7 @@ def _mark_stale(card_path: Path, today: date) -> None:
312
532
  def _card_refreshed(raw: str) -> date | None:
313
533
  for line in raw.splitlines():
314
534
  if line.startswith("refreshed:"):
315
- return _parse_day(line.split(":", 1)[1].strip())
535
+ return parse_day(line.split(":", 1)[1].strip())
316
536
  return None
317
537
 
318
538
 
@@ -356,7 +576,7 @@ def organize_repo(repo: Path, today: date | None = None) -> Compiled:
356
576
  _mark_stale(path, today)
357
577
  # Cards may now be stale; rebuild status from the same outbox tips.
358
578
  compiled = build_compiled(repo)
359
- (repo / "STATUS.md").write_text(render_status(compiled), encoding="utf-8")
579
+ (repo / "STATUS.md").write_text(render_status(compiled, today=today), encoding="utf-8")
360
580
  after = {path.name: path.read_bytes() for path in sorted(outbox.glob("*.md"))} if outbox.is_dir() else {}
361
581
  if before != after:
362
582
  raise RuntimeError("整理改写了信箱")
@@ -12,14 +12,14 @@ from topmind_handoff.package import render_package
12
12
 
13
13
  USAGE = """\
14
14
  用法:
15
- python3 scripts/compile_memory.py <记忆目录>
15
+ python3 scripts/compile_memory.py <记忆目录> [--today YYYY-MM-DD]
16
16
  python3 scripts/compile_memory.py --check <记忆目录>
17
17
  python3 scripts/compile_memory.py --init <目录> --private --origin <origin_id> [--role 场景] [--today YYYY-MM-DD]
18
18
  python3 scripts/compile_memory.py --init <目录> --public --origin <origin_id>
19
19
  python3 scripts/compile_memory.py --organize <记忆目录> [--today YYYY-MM-DD]
20
- python3 scripts/compile_memory.py --render <记忆目录> --origin <origin_id> [--out 文件]
20
+ python3 scripts/compile_memory.py --render <记忆目录> --origin <origin_id> [--out 文件] [--today YYYY-MM-DD]
21
21
  python3 scripts/compile_memory.py --own <记忆目录> --scenario <场景> --origin <origin_id>
22
- python3 scripts/compile_memory.py --startup <记忆目录> --origin <origin_id>
22
+ python3 scripts/compile_memory.py --startup <记忆目录> --origin <origin_id> [--today YYYY-MM-DD]
23
23
  """
24
24
 
25
25
 
@@ -48,9 +48,7 @@ def main(argv: list[str]) -> int:
48
48
  if argv[0].startswith("--"):
49
49
  print(USAGE)
50
50
  return 2
51
- compile_repo(Path(argv[0]))
52
- print(f"compiled {argv[0]}")
53
- return 0
51
+ return _compile(argv)
54
52
 
55
53
 
56
54
  def _init(argv: list[str]) -> int:
@@ -78,6 +76,17 @@ def _init(argv: list[str]) -> int:
78
76
  return 0
79
77
 
80
78
 
79
+ def _compile(argv: list[str]) -> int:
80
+ parser = argparse.ArgumentParser(add_help=False)
81
+ parser.add_argument("path")
82
+ parser.add_argument("--today")
83
+ args = parser.parse_args(argv)
84
+ today = _day(args.today) if args.today else None
85
+ compile_repo(Path(args.path), today)
86
+ print(f"compiled {args.path}")
87
+ return 0
88
+
89
+
81
90
  def _organize(argv: list[str]) -> int:
82
91
  parser = argparse.ArgumentParser(add_help=False)
83
92
  parser.add_argument("path")
@@ -94,9 +103,11 @@ def _render(argv: list[str]) -> int:
94
103
  parser.add_argument("path")
95
104
  parser.add_argument("--origin", required=True)
96
105
  parser.add_argument("--out")
106
+ parser.add_argument("--today")
97
107
  args = parser.parse_args(argv)
108
+ today = _day(args.today) if args.today else None
98
109
  try:
99
- text = render_package(Path(args.path), args.origin)
110
+ text = render_package(Path(args.path), args.origin, today=today)
100
111
  except ValueError as exc:
101
112
  print(str(exc))
102
113
  return 1
@@ -127,9 +138,11 @@ def _startup(argv: list[str]) -> int:
127
138
  parser = argparse.ArgumentParser(add_help=False)
128
139
  parser.add_argument("path")
129
140
  parser.add_argument("--origin", required=True)
141
+ parser.add_argument("--today")
130
142
  args = parser.parse_args(argv)
143
+ today = _day(args.today) if args.today else None
131
144
  try:
132
- print(startup_read(Path(args.path), args.origin), end="")
145
+ print(startup_read(Path(args.path), args.origin, today=today), end="")
133
146
  except ValueError as exc:
134
147
  print(str(exc))
135
148
  return 1