@topmindspace/topmind-handoff 0.7.2 → 0.7.3

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.
@@ -68,7 +68,8 @@ def memory_readme() -> str:
68
68
  "- 只在某个场景里成立的习惯用 `scope: scenario:<场景>`,不写入 shared。",
69
69
  "- 另一台要接着做的下一步用 `scope: shared` 或 `scope: scenario:<场景>`。只在这台的路径和工具状态用 `scope: origin:<origin_id>`。技能版本和连接器写在这台的环境卡片上,不写成共享事实。",
70
70
  "- 产出只记状态、下一步和位置,不记正文。`slice` 用 `work` 或 `project`。`~/` 路径用 `scope: origin:`。",
71
- "- 已有主题要改,沿用原来的 id,`base` 填当前链头,不另起一条 id。",
71
+ "- 日常工作默认每个环境维护一份自己的【当前工作】(`--work`,写进 `id: work.<origin_id>`,跨环境同步进展且互不冲突;仅本机可见加 `--local`);特定项目交接加 `--topic <专项名>`;完结加 `--done` 撤回。",
72
+ "- 已有主题要改,沿用原来的 id,`base` 填当前链头,不另起一条 id(可用 `--set`、`--work`、`--retract`、`--resolve` 自动算 `op_id` 和 `base`)。",
72
73
  "- 换范围时先在旧范围撤回,再在新范围用同一 id 写下,`base: none`。",
73
74
  "- 不写密码、API key、token,不写聊天记录,不改别人的信箱。",
74
75
  "",
@@ -179,178 +180,6 @@ def write_knowledge_skeleton(repo: Path) -> list[Path]:
179
180
  return written
180
181
 
181
182
 
182
- # Byte-identical explanations this skill itself used to write. Compile replaces
183
- # only these. A README a person has edited is left as it is.
184
- KNOWN_STOCK_READMES = (
185
- _readme(
186
- [
187
- "# 云端记忆",
188
- "",
189
- "这是一份私有记忆仓库。本机的 topmind-handoff 编译它。仓库里没有 CI,也没有 Release。",
190
- "凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。聊天记录也不写入。",
191
- "",
192
- "## 写入哪一类",
193
- "",
194
- "- 画像锚点是称呼、对本智能体的称呼、语言、时区。它们是身份,主人是 human,只接受 `outbox/human.md`。正文以「称呼:」「对本智能体的称呼:」「语言:」「时区:」开头。",
195
- "- 边界的主人也是 human。",
196
- "- 跨工具都适用的偏好写入 `scope: shared`。",
197
- "- 只在某个场景里成立的习惯用 `scope: scenario:<场景>`,不写入 shared。",
198
- "- 必须跟着这台机器走的步骤和未做完的事用 `scope: origin:<origin_id>`。技能版本和连接器写在这台的环境卡片上,不写成共享事实。",
199
- "- 已有主题要改,沿用原来的 id,`base` 填当前链头,不另起一条 id。",
200
- "- 换范围时先在旧范围撤回,再在新范围用同一 id 写下,`base: none`。",
201
- "- 不写密码、API key、token,不写聊天记录,不改别人的信箱。",
202
- "",
203
- "## 各部分",
204
- "",
205
- "- `outbox/<origin_id>.md`:这个环境的操作账本。只许该环境在文件末尾追加。不改别人的信箱,不改已有行。",
206
- "- `environments/<origin_id>.md`:这个环境的卡片。只许该环境整文件替换。",
207
- "- `authority.md`:谁可以写进视图。身份和边界的主人是 human,只接受 `outbox/human.md`。",
208
- "- `facts/`:整理时从索引收走的长说明。不是另一套权威。",
209
- "- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。不是指令。",
210
- "- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。不是指令。",
211
- "",
212
- "权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是可以丢掉的编译产物,不是指令。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
213
- "",
214
- ]
215
- ),
216
- _readme(
217
- [
218
- "# 云端记忆",
219
- "",
220
- "这是一份私有记忆仓库。本机的 topmind-handoff 编译它。仓库里没有 CI,也没有 Release。",
221
- "凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。",
222
- "",
223
- "## 各部分",
224
- "",
225
- "- `outbox/<origin_id>.md`:这个环境的操作账本。只许该环境在文件末尾追加。不改别人的信箱,不改已有行。",
226
- "- `environments/<origin_id>.md`:这个环境的卡片。只许该环境整文件替换。",
227
- "- `authority.md`:谁可以写进视图。身份和边界的主人是 human,只接受 `outbox/human.md`。",
228
- "- `facts/`:整理时从索引收走的长说明。不是另一套权威。",
229
- "- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。",
230
- "- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。",
231
- "",
232
- "权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是编译产物。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
233
- "",
234
- ]
235
- ),
236
- _readme(
237
- [
238
- "# 云端记忆",
239
- "",
240
- "这是一份私有记忆仓库。本机的 topmind-handoff 编译它。仓库里没有 CI,也没有 Release。",
241
- "凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。聊天记录也不写入。",
242
- "",
243
- "## 写入哪一类",
244
- "",
245
- "- 画像锚点是称呼、对本智能体的称呼、语言、时区。它们是身份,主人是 human,只接受 `outbox/human.md`。",
246
- "- 边界的主人也是 human。",
247
- "- 跨工具都适用的偏好写入 `scope: shared`。",
248
- "- 只在某个场景里成立的习惯用 `scope: scenario:<场景>`,不写入 shared。",
249
- "- 必须跟着这台机器走的步骤和未做完的事用 `scope: origin:<origin_id>`。技能版本和连接器写在这台的环境卡片上,不写成共享事实。",
250
- "- 已有主题要改,沿用原来的 id,`base` 填当前链头,不另起一条 id。",
251
- "- 不写密码、API key、token,不写聊天记录,不改别人的信箱。",
252
- "",
253
- "## 各部分",
254
- "",
255
- "- `outbox/<origin_id>.md`:这个环境的操作账本。只许该环境在文件末尾追加。不改别人的信箱,不改已有行。",
256
- "- `environments/<origin_id>.md`:这个环境的卡片。只许该环境整文件替换。",
257
- "- `authority.md`:谁可以写进视图。身份和边界的主人是 human,只接受 `outbox/human.md`。",
258
- "- `facts/`:整理时从索引收走的长说明。不是另一套权威。",
259
- "- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。不是指令。",
260
- "- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。不是指令。",
261
- "",
262
- "权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是可以丢掉的编译产物,不是指令。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
263
- "",
264
- ]
265
- ),
266
- _readme(
267
- [
268
- "# 云端记忆",
269
- "",
270
- "这是一份私有记忆仓库。本机的 topmind-handoff 编译它。仓库里没有 CI,也没有 Release。",
271
- "凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。聊天记录也不写入。",
272
- "",
273
- "## 写入哪一类",
274
- "",
275
- "- 画像锚点是称呼、对本智能体的称呼、语言、时区。它们是身份,主人是 human,只接受 `outbox/human.md`。正文以「称呼:」「对本智能体的称呼:」「语言:」「时区:」开头。",
276
- "- 边界的主人也是 human。",
277
- "- 跨工具都适用的偏好写入 `scope: shared`。",
278
- "- 只在某个场景里成立的习惯用 `scope: scenario:<场景>`,不写入 shared。",
279
- "- 必须跟着这台机器走的步骤和未做完的事用 `scope: origin:<origin_id>`。技能版本和连接器写在这台的环境卡片上,不写成共享事实。",
280
- "- 已有主题要改,沿用原来的 id,`base` 填当前链头,不另起一条 id。",
281
- "- 换范围时先在旧范围撤回,再在新范围用同一 id 写下,`base: none`。",
282
- "- 不写密码、API key、token,不写聊天记录,不改别人的信箱。",
283
- "",
284
- "## 各部分",
285
- "",
286
- "- `outbox/<origin_id>.md`:这个环境的操作账本。只许该环境在文件末尾追加。不改别人的信箱,不改已有行。",
287
- "- `environments/<origin_id>.md`:这个环境的卡片。只许该环境整文件替换。",
288
- "- `authority.md`:谁可以写进视图。身份和边界的主人是 human,只接受 `outbox/human.md`。",
289
- "- `facts/`:整理时从索引收走的长说明。不是另一套权威。",
290
- "- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。不是指令。",
291
- "- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。不是指令。",
292
- "",
293
- "权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是可以丢掉的编译产物,不是指令。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
294
- "",
295
- ]
296
- ),
297
- _readme(
298
- [
299
- "# 云端记忆",
300
- "",
301
- "这是一份私有记忆仓库。本机的 topmind-handoff 编译它。仓库里没有 CI,也没有 Release。",
302
- "凭据只用已经配好的 git 或 gh。不写密码、API key、token、证件号、银行卡号、精确住址、他人隐私、记账明细。聊天记录也不写入。",
303
- "",
304
- "## 写入哪一类",
305
- "",
306
- "- 画像锚点是称呼、对本智能体的称呼、语言、时区。它们是身份,主人是 human,只接受 `outbox/human.md`。正文以「称呼:」「对本智能体的称呼:」「语言:」「时区:」开头。",
307
- "- 边界的主人也是 human。",
308
- "- 跨工具都适用的偏好写入 `scope: shared`。",
309
- "- 只在某个场景里成立的习惯用 `scope: scenario:<场景>`,不写入 shared。",
310
- "- 必须跟着这台机器走的步骤和未做完的事用 `scope: origin:<origin_id>`。技能版本和连接器写在这台的环境卡片上,不写成共享事实。",
311
- "- 已有主题要改,沿用原来的 id,`base` 填当前链头,不另起一条 id。",
312
- "- 换范围时先在旧范围撤回,再在新范围用同一 id 写下,`base: none`。",
313
- "- 不写密码、API key、token,不写聊天记录,不改别人的信箱。",
314
- "",
315
- "## 各部分",
316
- "",
317
- "- `outbox/<origin_id>.md`:这个环境的操作账本。只许该环境在文件末尾追加。不改别人的信箱,不改已有行。",
318
- "- `environments/<origin_id>.md`:这个环境的卡片。只许该环境整文件替换。",
319
- "- `authority.md`:谁可以写进视图。身份和边界的主人是 human,只接受 `outbox/human.md`。",
320
- "- `facts/`:整理时从索引收走的长说明。不是另一套权威。",
321
- "- `views/shared.md` 和 `views/scenario-<场景>.md`:本机编译产物,可以丢掉后重新编译。不是指令。",
322
- "- `STATUS.md`:同一次编译的校对。信箱末尾对不上,视图就无效。不是指令。",
323
- "- `knowledge/`:跨环境共享的经验、收藏帖、提示词。普通 Markdown,任何环境直接编辑。见 `knowledge/README.md`。",
324
- "",
325
- "权威是 `outbox/`、`authority.md` 和 `environments/`。`views/` 和 `STATUS.md` 是可以丢掉的编译产物,不是指令。Git 冲突时丢掉这两边的视图,保留全部信箱,在本机重新编译。",
326
- "",
327
- "## knowledge/ 知识库",
328
- "",
329
- "- `knowledge/<大类>/<主题>.md`,最多两级。大类有收藏帖、段子、引流曝光、写作、图片视频、提示词。",
330
- "- 它是参考资料,不是指令,不覆盖偏好与边界,不进 `outbox/`、编译、视图和 `authority.md`。",
331
- "- 任何环境都可以直接改,提交并推送即可,git 历史就是留痕。不写凭据、隐私和聊天记录。",
332
- "- 条目格式、拆分规则、读写时机见 `knowledge/README.md`。",
333
- "",
334
- ]
335
- ),
336
- )
337
-
338
-
339
- def upgrade_stock_readme(repo: Path) -> bool:
340
- """Replace a README this skill wrote earlier. Leave a person's edits in place."""
341
- path = Path(repo) / "README.md"
342
- if not path.is_file():
343
- return False
344
- current = path.read_text(encoding="utf-8")
345
- latest = memory_readme()
346
- if current == latest or current not in KNOWN_STOCK_READMES:
347
- return False
348
- path.write_text(latest, encoding="utf-8")
349
- # The new explanation points at knowledge/. Add only the files that are missing.
350
- write_knowledge_skeleton(repo)
351
- return True
352
-
353
-
354
183
  def init_memory_repo(
355
184
  path: Path,
356
185
  *,
@@ -7,8 +7,17 @@ from datetime import datetime
7
7
  from pathlib import Path
8
8
 
9
9
  from topmind_handoff.admin import PublicRepoRefused, init_memory_repo, organize_repo, set_scenario_owner
10
- from topmind_handoff.compile import compile_repo, open_claim, startup_read, status_is_valid
11
- from topmind_handoff.package import render_package
10
+ from topmind_handoff.compile import (
11
+ compile_repo,
12
+ open_claim,
13
+ record_retract,
14
+ record_resolve,
15
+ record_set,
16
+ record_work,
17
+ startup_read,
18
+ status_is_valid,
19
+ )
20
+ from topmind_handoff.package import render_package, replay_package
12
21
 
13
22
  USAGE = """\
14
23
  用法:
@@ -16,6 +25,11 @@ USAGE = """\
16
25
  python3 scripts/compile_memory.py --check <记忆目录>
17
26
  python3 scripts/compile_memory.py --init <目录> --private --origin <origin_id> [--role 场景] [--today YYYY-MM-DD]
18
27
  python3 scripts/compile_memory.py --init <目录> --public --origin <origin_id>
28
+ python3 scripts/compile_memory.py --set <记忆目录> --origin <origin_id> --id <claim_id> --text <正文> [--scope 范围] [--slice 维度] [--base op_id] [--detail 长说明] [--today YYYY-MM-DD]
29
+ python3 scripts/compile_memory.py --retract <记忆目录> --origin <origin_id> --id <claim_id> [--scope 范围] [--today YYYY-MM-DD]
30
+ python3 scripts/compile_memory.py --resolve <记忆目录> --origin <origin_id> --id <claim_id> --text <正文> [--scope 范围] [--slice 维度] [--detail 长说明] [--today YYYY-MM-DD]
31
+ python3 scripts/compile_memory.py --work <记忆目录> --origin <origin_id> [--text 正文] [--topic 专项] [--local] [--done] [--detail 长说明] [--today YYYY-MM-DD]
32
+ python3 scripts/compile_memory.py --replay <记忆目录> --file <交接包> [--confirm]
19
33
  python3 scripts/compile_memory.py --organize <记忆目录> [--today YYYY-MM-DD]
20
34
  python3 scripts/compile_memory.py --open <记忆目录> --id <claim_id>
21
35
  python3 scripts/compile_memory.py --render <记忆目录> --origin <origin_id> [--out 文件] [--today YYYY-MM-DD]
@@ -25,7 +39,10 @@ USAGE = """\
25
39
 
26
40
 
27
41
  def _day(value: str):
28
- return datetime.strptime(value, "%Y-%m-%d").date()
42
+ try:
43
+ return datetime.strptime(value, "%Y-%m-%d").date()
44
+ except ValueError as exc:
45
+ raise ValueError(f"日期格式无效(应为 YYYY-MM-DD):{value}") from exc
29
46
 
30
47
 
31
48
  def main(argv: list[str]) -> int:
@@ -33,11 +50,24 @@ def main(argv: list[str]) -> int:
33
50
  print(USAGE)
34
51
  return 0 if argv else 2
35
52
  if argv[0] == "--check":
53
+ if len(argv) != 2:
54
+ print(USAGE)
55
+ return 2
36
56
  ok, message = status_is_valid(Path(argv[1]))
37
57
  print(message)
38
58
  return 0 if ok else 1
39
59
  if argv[0] == "--init":
40
60
  return _init(argv[1:])
61
+ if argv[0] == "--set":
62
+ return _set(argv[1:])
63
+ if argv[0] == "--retract":
64
+ return _retract(argv[1:])
65
+ if argv[0] == "--resolve":
66
+ return _resolve(argv[1:])
67
+ if argv[0] == "--work":
68
+ return _work(argv[1:])
69
+ if argv[0] == "--replay":
70
+ return _replay(argv[1:])
41
71
  if argv[0] == "--organize":
42
72
  return _organize(argv[1:])
43
73
  if argv[0] == "--open":
@@ -84,8 +114,12 @@ def _compile(argv: list[str]) -> int:
84
114
  parser.add_argument("path")
85
115
  parser.add_argument("--today")
86
116
  args = parser.parse_args(argv)
87
- today = _day(args.today) if args.today else None
88
- compile_repo(Path(args.path), today)
117
+ try:
118
+ today = _day(args.today) if args.today else None
119
+ compile_repo(Path(args.path), today)
120
+ except ValueError as exc:
121
+ print(str(exc))
122
+ return 1
89
123
  print(f"compiled {args.path}")
90
124
  return 0
91
125
 
@@ -95,8 +129,12 @@ def _organize(argv: list[str]) -> int:
95
129
  parser.add_argument("path")
96
130
  parser.add_argument("--today")
97
131
  args = parser.parse_args(argv)
98
- today = _day(args.today) if args.today else None
99
- organize_repo(Path(args.path), today)
132
+ try:
133
+ today = _day(args.today) if args.today else None
134
+ organize_repo(Path(args.path), today)
135
+ except ValueError as exc:
136
+ print(str(exc))
137
+ return 1
100
138
  print(f"organized {args.path}")
101
139
  return 0
102
140
 
@@ -121,8 +159,8 @@ def _render(argv: list[str]) -> int:
121
159
  parser.add_argument("--out")
122
160
  parser.add_argument("--today")
123
161
  args = parser.parse_args(argv)
124
- today = _day(args.today) if args.today else None
125
162
  try:
163
+ today = _day(args.today) if args.today else None
126
164
  text = render_package(Path(args.path), args.origin, today=today)
127
165
  except ValueError as exc:
128
166
  print(str(exc))
@@ -157,8 +195,8 @@ def _startup(argv: list[str]) -> int:
157
195
  parser.add_argument("--also")
158
196
  parser.add_argument("--today")
159
197
  args = parser.parse_args(argv)
160
- today = _day(args.today) if args.today else None
161
198
  try:
199
+ today = _day(args.today) if args.today else None
162
200
  print(startup_read(Path(args.path), args.origin, today=today, also=args.also), end="")
163
201
  except ValueError as exc:
164
202
  print(str(exc))
@@ -166,3 +204,141 @@ def _startup(argv: list[str]) -> int:
166
204
  return 0
167
205
 
168
206
 
207
+ def _set(argv: list[str]) -> int:
208
+ parser = argparse.ArgumentParser(add_help=False)
209
+ parser.add_argument("path")
210
+ parser.add_argument("--origin", required=True)
211
+ parser.add_argument("--id", required=True, dest="claim_id")
212
+ parser.add_argument("--text", required=True)
213
+ parser.add_argument("--scope", default="shared")
214
+ parser.add_argument("--slice", dest="slice_name")
215
+ parser.add_argument("--base")
216
+ parser.add_argument("--detail", default="")
217
+ parser.add_argument("--today")
218
+ args = parser.parse_args(argv)
219
+ try:
220
+ today = _day(args.today) if args.today else None
221
+ op_id = record_set(
222
+ Path(args.path),
223
+ origin=args.origin,
224
+ claim_id=args.claim_id,
225
+ text=args.text,
226
+ scope=args.scope,
227
+ slice_name=args.slice_name,
228
+ base=args.base,
229
+ detail=args.detail,
230
+ today=today,
231
+ )
232
+ except ValueError as exc:
233
+ print(str(exc))
234
+ return 1
235
+ print(f"set {op_id} ({args.scope} {args.claim_id})")
236
+ return 0
237
+
238
+
239
+ def _retract(argv: list[str]) -> int:
240
+ parser = argparse.ArgumentParser(add_help=False)
241
+ parser.add_argument("path")
242
+ parser.add_argument("--origin", required=True)
243
+ parser.add_argument("--id", required=True, dest="claim_id")
244
+ parser.add_argument("--scope", default="shared")
245
+ parser.add_argument("--text", default="已撤回")
246
+ parser.add_argument("--today")
247
+ args = parser.parse_args(argv)
248
+ try:
249
+ today = _day(args.today) if args.today else None
250
+ op_id = record_retract(
251
+ Path(args.path),
252
+ origin=args.origin,
253
+ claim_id=args.claim_id,
254
+ scope=args.scope,
255
+ text=args.text,
256
+ today=today,
257
+ )
258
+ except ValueError as exc:
259
+ print(str(exc))
260
+ return 1
261
+ print(f"retracted {op_id} ({args.scope} {args.claim_id})")
262
+ return 0
263
+
264
+
265
+ def _resolve(argv: list[str]) -> int:
266
+ parser = argparse.ArgumentParser(add_help=False)
267
+ parser.add_argument("path")
268
+ parser.add_argument("--origin", required=True)
269
+ parser.add_argument("--id", required=True, dest="claim_id")
270
+ parser.add_argument("--text", required=True)
271
+ parser.add_argument("--scope", default="shared")
272
+ parser.add_argument("--slice", dest="slice_name")
273
+ parser.add_argument("--detail", default="")
274
+ parser.add_argument("--today")
275
+ args = parser.parse_args(argv)
276
+ try:
277
+ today = _day(args.today) if args.today else None
278
+ op_id = record_resolve(
279
+ Path(args.path),
280
+ origin=args.origin,
281
+ claim_id=args.claim_id,
282
+ text=args.text,
283
+ scope=args.scope,
284
+ slice_name=args.slice_name,
285
+ detail=args.detail,
286
+ today=today,
287
+ )
288
+ except ValueError as exc:
289
+ print(str(exc))
290
+ return 1
291
+ print(f"resolved {op_id} ({args.scope} {args.claim_id})")
292
+ return 0
293
+
294
+
295
+ def _work(argv: list[str]) -> int:
296
+ parser = argparse.ArgumentParser(add_help=False)
297
+ parser.add_argument("path")
298
+ parser.add_argument("--origin", required=True)
299
+ parser.add_argument("--text", default="")
300
+ parser.add_argument("--topic")
301
+ parser.add_argument("--scope")
302
+ parser.add_argument("--local", action="store_true")
303
+ parser.add_argument("--done", action="store_true")
304
+ parser.add_argument("--detail", default="")
305
+ parser.add_argument("--today")
306
+ args = parser.parse_args(argv)
307
+ try:
308
+ today = _day(args.today) if args.today else None
309
+ op_id = record_work(
310
+ Path(args.path),
311
+ origin=args.origin,
312
+ text=args.text,
313
+ topic=args.topic,
314
+ scope=args.scope,
315
+ local=args.local,
316
+ done=args.done,
317
+ detail=args.detail,
318
+ today=today,
319
+ )
320
+ except ValueError as exc:
321
+ print(str(exc))
322
+ return 1
323
+ action = "done" if args.done else "work"
324
+ print(f"{action} {op_id}")
325
+ return 0
326
+
327
+
328
+ def _replay(argv: list[str]) -> int:
329
+ parser = argparse.ArgumentParser(add_help=False)
330
+ parser.add_argument("path")
331
+ parser.add_argument("--file", required=True, dest="pkg_file")
332
+ parser.add_argument("--confirm", action="store_true")
333
+ args = parser.parse_args(argv)
334
+ try:
335
+ pkg_text = Path(args.pkg_file).read_text(encoding="utf-8")
336
+ added = replay_package(Path(args.path), pkg_text, confirmed=args.confirm)
337
+ except (ValueError, OSError) as exc:
338
+ print(str(exc))
339
+ return 1
340
+ mode = "replayed" if args.confirm else "preview"
341
+ print(f"{mode}: {', '.join(added) if added else 'none'}")
342
+ return 0
343
+
344
+