memoryledger 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
memoryledger/cli.py ADDED
@@ -0,0 +1,700 @@
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import typer
9
+
10
+ from . import __version__
11
+ from .adopt import adopt, parse_markdown
12
+ from .errors import MemoryledgerError
13
+ from .evidence_scan import apply as apply_scan
14
+ from .evidence_scan import scan as scan_evidence
15
+ from .guardrails import validate_memory, validate_scope_path
16
+ from .intake import import_run_html as intake_run_html
17
+ from .intake import import_text as intake_text
18
+ from .models import EVIDENCE_KINDS, KINDS, RENDER_TARGETS, SCOPES, Config, EvidenceRef
19
+ from .render import export as export_rendered
20
+ from .render import render_all, write_rendered
21
+ from .review import transition
22
+ from .storage import Store, find_config, init_workspace, load_config
23
+ from .templates import (
24
+ apply_template,
25
+ find_template,
26
+ load_global_config,
27
+ remove_template,
28
+ template_content,
29
+ )
30
+
31
+ app = typer.Typer(no_args_is_help=True)
32
+ memory_app = typer.Typer(no_args_is_help=True)
33
+ evidence_app = typer.Typer(no_args_is_help=True)
34
+ review_app = typer.Typer(no_args_is_help=True)
35
+ import_app = typer.Typer(no_args_is_help=True)
36
+ agents_app = typer.Typer(no_args_is_help=True)
37
+ templates_app = typer.Typer(no_args_is_help=True)
38
+ scan_app = typer.Typer(no_args_is_help=True)
39
+ app.add_typer(memory_app, name="memory")
40
+ memory_app.add_typer(evidence_app, name="evidence")
41
+ app.add_typer(review_app, name="review")
42
+ app.add_typer(import_app, name="import")
43
+ app.add_typer(agents_app, name="agents")
44
+ app.add_typer(templates_app, name="templates")
45
+ app.add_typer(scan_app, name="evidence")
46
+
47
+
48
+ def _json(data: object) -> None:
49
+ typer.echo(json.dumps(data, indent=2, sort_keys=True))
50
+
51
+
52
+ def _load() -> tuple[Config, Store]:
53
+ config = load_config()
54
+ return config, Store(config)
55
+
56
+
57
+ def _read_input(text: str | None, file: Path | None, stdin: bool) -> str:
58
+ choices = sum([text is not None, file is not None, stdin])
59
+ if choices != 1:
60
+ raise MemoryledgerError(
61
+ "INPUT_REQUIRED", "Provide exactly one of --text, --file, or --stdin"
62
+ )
63
+ if text is not None:
64
+ return text
65
+ if file is not None:
66
+ return file.read_text()
67
+ return sys.stdin.read()
68
+
69
+
70
+ def _handle_error(exc: Exception) -> None:
71
+ if isinstance(exc, MemoryledgerError):
72
+ typer.echo(f"error: {exc.code}: {exc.message}", err=True)
73
+ raise typer.Exit(1) from exc
74
+ raise exc
75
+
76
+
77
+ @app.callback()
78
+ def main(
79
+ version: bool = typer.Option(False, "--version", help="Show version and exit."),
80
+ ) -> None:
81
+ if version:
82
+ typer.echo(__version__)
83
+ raise typer.Exit()
84
+
85
+
86
+ @app.command()
87
+ def init(
88
+ project_name: str | None = None,
89
+ memoryledger_dir: str = ".memoryledger",
90
+ hidden_config: bool = False,
91
+ ) -> None:
92
+ try:
93
+ config = init_workspace(project_name, memoryledger_dir, hidden_config)
94
+ typer.echo(f"initialized {config.config_path}")
95
+ except Exception as exc:
96
+ _handle_error(exc)
97
+
98
+
99
+ @app.command()
100
+ def status(
101
+ check: bool = False, json_output: bool = typer.Option(False, "--json")
102
+ ) -> None:
103
+ try:
104
+ config_path = find_config()
105
+ if config_path is None:
106
+ data = {"ok": False, "configured": False}
107
+ if json_output:
108
+ _json(data)
109
+ else:
110
+ typer.echo("No memoryledger config found.")
111
+ if check:
112
+ raise typer.Exit(1)
113
+ return
114
+ config, store = _load()
115
+ memories = store.all_memories()
116
+ status_data: dict[str, object] = {
117
+ "ok": True,
118
+ "configured": True,
119
+ "config": str(config.config_path),
120
+ "storage": str(config.storage_dir),
121
+ "memories": len(memories),
122
+ }
123
+ if json_output:
124
+ _json(status_data)
125
+ else:
126
+ typer.echo(f"config: {config.config_path}")
127
+ typer.echo(f"storage: {config.storage_dir}")
128
+ typer.echo(f"memories: {len(memories)}")
129
+ except Exception as exc:
130
+ _handle_error(exc)
131
+
132
+
133
+ @app.command()
134
+ def doctor(json_output: bool = typer.Option(False, "--json")) -> None:
135
+ try:
136
+ config, _store = _load()
137
+ issues: list[str] = []
138
+ if not config.storage_dir.exists():
139
+ issues.append("storage directory missing")
140
+ data = {"ok": not issues, "issues": issues}
141
+ if json_output:
142
+ _json(data)
143
+ else:
144
+ typer.echo("ok" if not issues else "issues: " + ", ".join(issues))
145
+ except Exception as exc:
146
+ _handle_error(exc)
147
+
148
+
149
+ @app.command()
150
+ def info(
151
+ json_output: bool = typer.Option(False, "--json"),
152
+ paths_only: bool = False,
153
+ no_content: bool = False,
154
+ ) -> None:
155
+ try:
156
+ config, _store = _load()
157
+ data = {
158
+ "config": str(config.config_path),
159
+ "root": str(config.root),
160
+ "storage": str(config.storage_dir),
161
+ }
162
+ if json_output:
163
+ _json(data)
164
+ else:
165
+ for value in data.values() if paths_only else data.items():
166
+ typer.echo(value if paths_only else f"{value[0]}: {value[1]}")
167
+ except Exception as exc:
168
+ _handle_error(exc)
169
+
170
+
171
+ @memory_app.command("create")
172
+ def memory_create(
173
+ kind: str = typer.Option(..., "--kind"),
174
+ title: str = typer.Option(..., "--title"),
175
+ text: str | None = typer.Option(None, "--text"),
176
+ file: Path | None = typer.Option(None, "--file"),
177
+ stdin: bool = typer.Option(False, "--stdin"),
178
+ evidence: str = typer.Option("", "--evidence"),
179
+ scope: str = typer.Option("global", "--scope"),
180
+ scope_path: str = typer.Option("", "--scope-path"),
181
+ render_target: str = typer.Option("root_agents", "--render-target"),
182
+ section: str = typer.Option("", "--section"),
183
+ ) -> None:
184
+ try:
185
+ if (
186
+ kind not in KINDS
187
+ or scope not in SCOPES
188
+ or render_target not in RENDER_TARGETS
189
+ ):
190
+ raise MemoryledgerError(
191
+ "INVALID_ARGUMENT", "invalid kind, scope, or render target"
192
+ )
193
+ config, store = _load()
194
+ validate_scope_path(config.root, scope_path)
195
+ content = _read_input(text, file, stdin)
196
+ memory = store.create(
197
+ kind,
198
+ title,
199
+ content,
200
+ evidence,
201
+ scope,
202
+ scope_path,
203
+ render_target,
204
+ section=section,
205
+ )
206
+ typer.echo(memory.id)
207
+ except Exception as exc:
208
+ _handle_error(exc)
209
+
210
+
211
+ @memory_app.command("list")
212
+ def memory_list(
213
+ kind: str | None = typer.Option(None, "--kind"),
214
+ status: str | None = typer.Option(None, "--status"),
215
+ json_output: bool = typer.Option(False, "--json"),
216
+ ) -> None:
217
+ try:
218
+ _config, store = _load()
219
+ items = [
220
+ m
221
+ for m in store.all_memories()
222
+ if (kind is None or m.kind == kind)
223
+ and (status is None or m.status == status)
224
+ ]
225
+ if json_output:
226
+ _json({"memories": [m.to_dict() for m in items]})
227
+ else:
228
+ for m in items:
229
+ typer.echo(f"{m.id} {m.status} {m.kind} {m.title}")
230
+ except Exception as exc:
231
+ _handle_error(exc)
232
+
233
+
234
+ @memory_app.command("show")
235
+ def memory_show(
236
+ memory_id: str,
237
+ content: bool = False,
238
+ json_output: bool = typer.Option(False, "--json"),
239
+ ) -> None:
240
+ try:
241
+ _config, store = _load()
242
+ memory = store.get(memory_id)
243
+ body = store.read_content(memory_id)
244
+ if json_output:
245
+ data = memory.to_dict()
246
+ if content:
247
+ data["content"] = body
248
+ _json(data)
249
+ else:
250
+ typer.echo(f"{memory.id} {memory.status} {memory.kind} {memory.title}")
251
+ if content:
252
+ typer.echo(body.rstrip())
253
+ except Exception as exc:
254
+ _handle_error(exc)
255
+
256
+
257
+ @memory_app.command("status")
258
+ def memory_status(
259
+ memory_id: str, status: str, reason: str = typer.Option(..., "--reason")
260
+ ) -> None:
261
+ try:
262
+ _config, store = _load()
263
+ memory = transition(store, memory_id, status, reason)
264
+ typer.echo(f"{memory.id} {memory.status}")
265
+ except Exception as exc:
266
+ _handle_error(exc)
267
+
268
+
269
+ @memory_app.command("edit")
270
+ def memory_edit(
271
+ memory_id: str,
272
+ reason: str = typer.Option(..., "--reason"),
273
+ text: str | None = typer.Option(None, "--text"),
274
+ file: Path | None = typer.Option(None, "--file"),
275
+ stdin: bool = typer.Option(False, "--stdin"),
276
+ section: str | None = typer.Option(None, "--section"),
277
+ ) -> None:
278
+ try:
279
+ _config, store = _load()
280
+ memory = store.update_content(
281
+ memory_id, _read_input(text, file, stdin), reason, section=section
282
+ )
283
+ typer.echo(f"{memory.id} v{memory.version:04d}")
284
+ except Exception as exc:
285
+ _handle_error(exc)
286
+
287
+
288
+ @memory_app.command("append")
289
+ def memory_append(
290
+ memory_id: str,
291
+ reason: str = typer.Option(..., "--reason"),
292
+ text: str | None = typer.Option(None, "--text"),
293
+ file: Path | None = typer.Option(None, "--file"),
294
+ stdin: bool = typer.Option(False, "--stdin"),
295
+ ) -> None:
296
+ try:
297
+ _config, store = _load()
298
+ memory = store.update_content(
299
+ memory_id, _read_input(text, file, stdin), reason, append=True
300
+ )
301
+ typer.echo(f"{memory.id} v{memory.version:04d}")
302
+ except Exception as exc:
303
+ _handle_error(exc)
304
+
305
+
306
+ @memory_app.command("validate")
307
+ def memory_validate(
308
+ memory_id: str, json_output: bool = typer.Option(False, "--json")
309
+ ) -> None:
310
+ try:
311
+ _config, store = _load()
312
+ memory = store.get(memory_id)
313
+ validate_memory(
314
+ memory,
315
+ store.read_content(memory_id),
316
+ store.read_evidence(memory_id),
317
+ store.config.root,
318
+ )
319
+ if json_output:
320
+ _json({"ok": True, "memory_id": memory_id})
321
+ else:
322
+ typer.echo("ok")
323
+ except Exception as exc:
324
+ _handle_error(exc)
325
+
326
+
327
+ @memory_app.command("versions")
328
+ def memory_versions(
329
+ memory_id: str, json_output: bool = typer.Option(False, "--json")
330
+ ) -> None:
331
+ try:
332
+ _config, store = _load()
333
+ versions = sorted(
334
+ p.stem for p in (store.memory_dir(memory_id) / "versions").glob("v*.yaml")
335
+ )
336
+ if json_output:
337
+ _json({"versions": versions})
338
+ else:
339
+ typer.echo("\n".join(versions))
340
+ except Exception as exc:
341
+ _handle_error(exc)
342
+
343
+
344
+ @memory_app.command("diff")
345
+ def memory_diff(
346
+ memory_id: str,
347
+ from_version: str = typer.Option(..., "--from"),
348
+ to: str = typer.Option(..., "--to"),
349
+ ) -> None:
350
+ try:
351
+ _config, store = _load()
352
+ base = store.memory_dir(memory_id) / "versions"
353
+ a = (base / f"{from_version}.md").read_text().splitlines()
354
+ b = (base / f"{to}.md").read_text().splitlines()
355
+ typer.echo("\n".join(difflib.unified_diff(a, b, from_version, to)))
356
+ except Exception as exc:
357
+ _handle_error(exc)
358
+
359
+
360
+ @evidence_app.command("list")
361
+ def evidence_list(
362
+ memory_id: str, json_output: bool = typer.Option(False, "--json")
363
+ ) -> None:
364
+ try:
365
+ _config, store = _load()
366
+ refs = [ref.to_dict() for ref in store.get(memory_id).evidence_refs]
367
+ if json_output:
368
+ _json({"memory_id": memory_id, "evidence": refs})
369
+ else:
370
+ for ref in refs:
371
+ typer.echo(f"{ref['kind']} {ref['title']} {ref['uri']}")
372
+ except Exception as exc:
373
+ _handle_error(exc)
374
+
375
+
376
+ @evidence_app.command("add")
377
+ def evidence_add(
378
+ memory_id: str,
379
+ kind: str = typer.Option(..., "--kind"),
380
+ title: str = typer.Option(..., "--title"),
381
+ uri: str = typer.Option(..., "--uri"),
382
+ reason: str = typer.Option(..., "--reason"),
383
+ excerpt: str = typer.Option("", "--excerpt"),
384
+ line_start: int | None = typer.Option(None, "--line-start"),
385
+ line_end: int | None = typer.Option(None, "--line-end"),
386
+ json_output: bool = typer.Option(False, "--json"),
387
+ ) -> None:
388
+ try:
389
+ if kind not in EVIDENCE_KINDS:
390
+ raise MemoryledgerError("INVALID_EVIDENCE_KIND", kind)
391
+ _config, store = _load()
392
+ memory = store.add_evidence(
393
+ memory_id,
394
+ EvidenceRef(
395
+ kind=kind,
396
+ title=title,
397
+ uri=uri,
398
+ excerpt=excerpt,
399
+ line_start=line_start,
400
+ line_end=line_end,
401
+ ),
402
+ reason,
403
+ )
404
+ data = {"memory_id": memory.id, "version": memory.version}
405
+ _json(data) if json_output else typer.echo(f"{memory.id} v{memory.version:04d}")
406
+ except Exception as exc:
407
+ _handle_error(exc)
408
+
409
+
410
+ @review_app.command("list")
411
+ def review_list(json_output: bool = typer.Option(False, "--json")) -> None:
412
+ return memory_list(status="candidate", json_output=json_output)
413
+
414
+
415
+ @review_app.command("accept")
416
+ def review_accept(memory_id: str, reason: str = typer.Option(..., "--reason")) -> None:
417
+ memory_status(memory_id, "accepted", reason)
418
+
419
+
420
+ @review_app.command("reject")
421
+ def review_reject(memory_id: str, reason: str = typer.Option(..., "--reason")) -> None:
422
+ memory_status(memory_id, "rejected", reason)
423
+
424
+
425
+ @review_app.command("archive")
426
+ def review_archive(memory_id: str, reason: str = typer.Option(..., "--reason")) -> None:
427
+ memory_status(memory_id, "archived", reason)
428
+
429
+
430
+ @app.command()
431
+ def render(
432
+ out: Path | None = None,
433
+ print_output: bool = typer.Option(False, "--print"),
434
+ json_output: bool = typer.Option(False, "--json"),
435
+ ) -> None:
436
+ try:
437
+ config = load_config()
438
+ result = render_all(config)
439
+ written = write_rendered(config, result, out)
440
+ if print_output:
441
+ typer.echo(result.root_text, nl=False)
442
+ if json_output:
443
+ _json(
444
+ {
445
+ "root": str(written[0]),
446
+ "linked_docs": sorted(result.linked_docs),
447
+ "nested_docs": sorted(result.nested_docs),
448
+ }
449
+ )
450
+ except Exception as exc:
451
+ _handle_error(exc)
452
+
453
+
454
+ @app.command()
455
+ def export(
456
+ out: Path | None = None,
457
+ json_output: bool = typer.Option(False, "--json"),
458
+ backup: bool = False,
459
+ include_nested: bool = False,
460
+ ) -> None:
461
+ try:
462
+ config = load_config()
463
+ result = render_all(config)
464
+ written = export_rendered(config, result, out, backup, include_nested)
465
+ if json_output:
466
+ _json({"written": [str(p) for p in written]})
467
+ else:
468
+ for path in written:
469
+ typer.echo(path)
470
+ except Exception as exc:
471
+ _handle_error(exc)
472
+
473
+
474
+ @agents_app.command("plan")
475
+ def agents_plan(json_output: bool = typer.Option(False, "--json")) -> None:
476
+ data = {"commands": ["memoryledger render", "memoryledger export"]}
477
+ _json(data) if json_output else typer.echo(
478
+ "memoryledger render\nmemoryledger export"
479
+ )
480
+
481
+
482
+ @agents_app.command("render")
483
+ def agents_render(json_output: bool = typer.Option(False, "--json")) -> None:
484
+ render(json_output=json_output)
485
+
486
+
487
+ @agents_app.command("export")
488
+ def agents_export(json_output: bool = typer.Option(False, "--json")) -> None:
489
+ export(json_output=json_output)
490
+
491
+
492
+ @agents_app.command("adopt")
493
+ def agents_adopt(
494
+ target: Path = typer.Argument(Path("AGENTS.md")),
495
+ apply: bool = typer.Option(False, "--apply"),
496
+ backup: bool = typer.Option(False, "--backup"),
497
+ accept: bool = typer.Option(False, "--accept"),
498
+ reason: str = typer.Option("", "--reason"),
499
+ json_output: bool = typer.Option(False, "--json"),
500
+ ) -> None:
501
+ try:
502
+ config, store = _load()
503
+ path = target if target.is_absolute() else config.root / target
504
+ try:
505
+ path.resolve().relative_to(config.root.resolve())
506
+ except ValueError as exc:
507
+ raise MemoryledgerError(
508
+ "INVALID_ADOPTION_PATH", "adoption target must be in the workspace"
509
+ ) from exc
510
+ if accept and not reason.strip():
511
+ raise MemoryledgerError(
512
+ "MISSING_REASON", "--accept requires a review reason"
513
+ )
514
+ digest, proposals = parse_markdown(path, config.root)
515
+ if not apply:
516
+ data = {
517
+ "target": str(path),
518
+ "source_hash": digest,
519
+ "proposals": [proposal.to_dict() for proposal in proposals],
520
+ "mutated": False,
521
+ }
522
+ else:
523
+ ids, backup_path = adopt(
524
+ store, path, backup=backup, accept=accept, reason=reason
525
+ )
526
+ data = {
527
+ "target": str(path),
528
+ "candidates": ids,
529
+ "backup": str(backup_path),
530
+ "mutated": True,
531
+ }
532
+ if json_output:
533
+ _json(data)
534
+ elif apply:
535
+ typer.echo("\n".join(ids))
536
+ else:
537
+ for proposal in proposals:
538
+ typer.echo(f"{proposal.kind} {proposal.title}")
539
+ except Exception as exc:
540
+ _handle_error(exc)
541
+
542
+
543
+ @templates_app.command("list")
544
+ def templates_list(json_output: bool = typer.Option(False, "--json")) -> None:
545
+ try:
546
+ templates = load_global_config().templates
547
+ data = [
548
+ {"id": item.id, "version": item.version, "title": item.title}
549
+ for item in templates
550
+ ]
551
+ if json_output:
552
+ _json({"templates": data})
553
+ else:
554
+ for item in data:
555
+ typer.echo(f"{item['id']} {item['version']} {item['title']}")
556
+ except Exception as exc:
557
+ _handle_error(exc)
558
+
559
+
560
+ @templates_app.command("show")
561
+ def templates_show(
562
+ template_id: str, json_output: bool = typer.Option(False, "--json")
563
+ ) -> None:
564
+ try:
565
+ template = find_template(load_global_config(), template_id)
566
+ data = {
567
+ "id": template.id,
568
+ "version": template.version,
569
+ "title": template.title,
570
+ "kind": template.kind,
571
+ "content": template_content(template),
572
+ }
573
+ _json(data) if json_output else typer.echo(data["content"], nl=False)
574
+ except Exception as exc:
575
+ _handle_error(exc)
576
+
577
+
578
+ def _template_apply(
579
+ template_id: str,
580
+ json_output: bool,
581
+ *,
582
+ accept: bool = False,
583
+ reason: str = "",
584
+ ) -> None:
585
+ config, store = _load()
586
+ if config.template_policy.ids and template_id not in config.template_policy.ids:
587
+ raise MemoryledgerError("TEMPLATE_NOT_ENABLED", template_id)
588
+ if accept and not reason.strip():
589
+ raise MemoryledgerError("MISSING_REASON", "--accept requires a review reason")
590
+ template = find_template(load_global_config(), template_id)
591
+ action, memory = apply_template(store, template)
592
+ if accept and memory.status != "accepted":
593
+ memory = transition(store, memory.id, "accepted", reason)
594
+ data = {"action": action, "memory_id": memory.id, "status": memory.status}
595
+ _json(data) if json_output else typer.echo(f"{action} {memory.id}")
596
+
597
+
598
+ @templates_app.command("apply")
599
+ def templates_apply(
600
+ template_id: str,
601
+ accept: bool = typer.Option(False, "--accept"),
602
+ reason: str = typer.Option("", "--reason"),
603
+ json_output: bool = typer.Option(False, "--json"),
604
+ ) -> None:
605
+ try:
606
+ _template_apply(template_id, json_output, accept=accept, reason=reason)
607
+ except Exception as exc:
608
+ _handle_error(exc)
609
+
610
+
611
+ @templates_app.command("sync")
612
+ def templates_sync(
613
+ template_id: str,
614
+ accept: bool = typer.Option(False, "--accept"),
615
+ reason: str = typer.Option("", "--reason"),
616
+ json_output: bool = typer.Option(False, "--json"),
617
+ ) -> None:
618
+ try:
619
+ _template_apply(template_id, json_output, accept=accept, reason=reason)
620
+ except Exception as exc:
621
+ _handle_error(exc)
622
+
623
+
624
+ @templates_app.command("remove")
625
+ def templates_remove(
626
+ template_id: str,
627
+ reason: str = typer.Option(..., "--reason"),
628
+ json_output: bool = typer.Option(False, "--json"),
629
+ ) -> None:
630
+ try:
631
+ _config, store = _load()
632
+ memory = remove_template(store, template_id, reason)
633
+ data = {"memory_id": memory.id, "status": memory.status}
634
+ _json(data) if json_output else typer.echo(f"{memory.id} archived")
635
+ except Exception as exc:
636
+ _handle_error(exc)
637
+
638
+
639
+ @scan_app.command("scan")
640
+ def evidence_scan(
641
+ apply_candidates: bool = typer.Option(False, "--apply-candidates"),
642
+ json_output: bool = typer.Option(False, "--json"),
643
+ ) -> None:
644
+ try:
645
+ config, store = _load()
646
+ proposals = scan_evidence(config.root)
647
+ results = apply_scan(store, proposals) if apply_candidates else []
648
+ data = {
649
+ "proposals": [proposal.to_dict() for proposal in proposals],
650
+ "applied": results,
651
+ }
652
+ if json_output:
653
+ _json(data)
654
+ elif apply_candidates:
655
+ for result in results:
656
+ typer.echo(f"{result['action']} {result['memory_id']}")
657
+ else:
658
+ for proposal in proposals:
659
+ typer.echo(f"{proposal.path}:{proposal.line} {proposal.observed}")
660
+ except Exception as exc:
661
+ _handle_error(exc)
662
+
663
+
664
+ @import_app.command("text")
665
+ def import_text_cmd(
666
+ text: str | None = typer.Option(None, "--text"),
667
+ file: Path | None = typer.Option(None, "--file"),
668
+ stdin: bool = typer.Option(False, "--stdin"),
669
+ json_output: bool = typer.Option(False, "--json"),
670
+ ) -> None:
671
+ try:
672
+ _config, store = _load()
673
+ ids = intake_text(store, _read_input(text, file, stdin))
674
+ _json({"candidates": ids}) if json_output else typer.echo("\n".join(ids))
675
+ except Exception as exc:
676
+ _handle_error(exc)
677
+
678
+
679
+ @import_app.command("run-html")
680
+ def import_run_html_cmd(
681
+ file: Path = typer.Option(..., "--file"),
682
+ json_output: bool = typer.Option(False, "--json"),
683
+ ) -> None:
684
+ try:
685
+ _config, store = _load()
686
+ ids = intake_run_html(store, file)
687
+ _json({"candidates": ids}) if json_output else typer.echo("\n".join(ids))
688
+ except Exception as exc:
689
+ _handle_error(exc)
690
+
691
+
692
+ @import_app.command("current-run")
693
+ def import_current_run(json_output: bool = typer.Option(False, "--json")) -> None:
694
+ data = {
695
+ "ok": False,
696
+ "unsupported": True,
697
+ "message": "current-run import is not supported in this runtime",
698
+ }
699
+ _json(data) if json_output else typer.echo(data["message"])
700
+ raise typer.Exit(2)