hypercli-cli 2026.5.21__tar.gz → 2026.6.21__tar.gz

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 (49) hide show
  1. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/PKG-INFO +5 -4
  2. hypercli_cli-2026.6.21/hypercli_cli/__init__.py +1 -0
  3. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/agent.py +5 -1
  4. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/agents.py +42 -1
  5. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/cli.py +28 -1
  6. hypercli_cli-2026.6.21/hypercli_cli/memory.py +334 -0
  7. hypercli_cli-2026.6.21/hypercli_cli/memory_captions.py +417 -0
  8. hypercli_cli-2026.6.21/hypercli_cli/memory_documents.py +330 -0
  9. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/output.py +22 -4
  10. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/pyproject.toml +5 -4
  11. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_agent_subscribe_command.py +31 -1
  12. hypercli_cli-2026.6.21/tests/test_agents_budget_command.py +54 -0
  13. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_exec_shell_dryrun.py +28 -0
  14. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_keys_command.py +1 -1
  15. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_me_command.py +56 -0
  16. hypercli_cli-2026.6.21/tests/test_memory_captions.py +265 -0
  17. hypercli_cli-2026.6.21/tests/test_memory_command.py +14 -0
  18. hypercli_cli-2026.6.21/tests/test_memory_documents.py +190 -0
  19. hypercli_cli-2026.6.21/tests/test_status_command.py +26 -0
  20. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_voice_command.py +9 -1
  21. hypercli_cli-2026.5.21/hypercli_cli/__init__.py +0 -1
  22. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/.gitignore +0 -0
  23. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/README.md +0 -0
  24. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/billing.py +0 -0
  25. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/comfyui.py +0 -0
  26. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/embed.py +0 -0
  27. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/files.py +0 -0
  28. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/flow.py +0 -0
  29. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/instances.py +0 -0
  30. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/jobs.py +0 -0
  31. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/keys.py +0 -0
  32. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/llm.py +0 -0
  33. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/onboard.py +0 -0
  34. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/renders.py +0 -0
  35. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/stt.py +0 -0
  36. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/tui/__init__.py +0 -0
  37. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/tui/job_monitor.py +0 -0
  38. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/user.py +0 -0
  39. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/voice.py +0 -0
  40. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/wallet.py +0 -0
  41. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_agent_env_resolution.py +0 -0
  42. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_config_command.py +0 -0
  43. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_flow_command.py +0 -0
  44. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_flow_visibility.py +0 -0
  45. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_jobs_list_tags.py +0 -0
  46. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_llm_command.py +0 -0
  47. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_openclaw_config.py +0 -0
  48. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_wallet_command.py +0 -0
  49. {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_wallet_migration_integration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypercli-cli
3
- Version: 2026.5.21
3
+ Version: 2026.6.21
4
4
  Summary: CLI for HyperCLI - GPU orchestration and LLM API
5
5
  Project-URL: Homepage, https://hypercli.com
6
6
  Project-URL: Documentation, https://docs.hypercli.com
@@ -9,9 +9,10 @@ Author-email: HyperCLI <support@hypercli.com>
9
9
  License: MIT
10
10
  Requires-Python: >=3.10
11
11
  Requires-Dist: httpx>=0.27.0
12
- Requires-Dist: hypercli-sdk>=2026.5.21
12
+ Requires-Dist: hypercli-sdk>=2026.6.21
13
13
  Requires-Dist: mutagen>=1.47.0
14
14
  Requires-Dist: openai>=2.8.1
15
+ Requires-Dist: pysubs2>=1.8.0
15
16
  Requires-Dist: pyyaml>=6.0
16
17
  Requires-Dist: rich>=14.2.0
17
18
  Requires-Dist: typer>=0.20.0
@@ -19,11 +20,11 @@ Provides-Extra: all
19
20
  Requires-Dist: argon2-cffi>=25.0.0; extra == 'all'
20
21
  Requires-Dist: eth-account>=0.13.0; extra == 'all'
21
22
  Requires-Dist: faster-whisper>=1.1.0; extra == 'all'
22
- Requires-Dist: hypercli-sdk[comfyui]>=2026.5.21; extra == 'all'
23
+ Requires-Dist: hypercli-sdk[comfyui]>=2026.6.21; extra == 'all'
23
24
  Requires-Dist: web3>=7.0.0; extra == 'all'
24
25
  Requires-Dist: x402[evm,httpx]>=2.0.0; extra == 'all'
25
26
  Provides-Extra: comfyui
26
- Requires-Dist: hypercli-sdk[comfyui]>=2026.5.21; extra == 'comfyui'
27
+ Requires-Dist: hypercli-sdk[comfyui]>=2026.6.21; extra == 'comfyui'
27
28
  Provides-Extra: dev
28
29
  Requires-Dist: pytest>=8.0.0; extra == 'dev'
29
30
  Requires-Dist: ruff>=0.3.0; extra == 'dev'
@@ -0,0 +1 @@
1
+ __version__ = "2026.6.21"
@@ -502,11 +502,15 @@ def subscription_summary(
502
502
  def activate_code(
503
503
  code: str = typer.Argument(..., help="Activation or promo code to redeem"),
504
504
  dev: bool = typer.Option(False, "--dev", help="Use dev API"),
505
+ extend_existing: bool = typer.Option(False, "--extend-existing", help="Extend the matching entitlement instead of creating a new one"),
505
506
  json_output: bool = typer.Option(False, "--json", help="Print raw JSON response"),
506
507
  ):
507
508
  """Redeem a HyperClaw activation code for the current account."""
508
509
  client = _get_agent_query_client(dev)
509
- result = client.agent.redeem_grant_code(code)
510
+ result = client.agent.redeem_grant_code(
511
+ code,
512
+ extend_existing=True if extend_existing else None,
513
+ )
510
514
 
511
515
  if json_output:
512
516
  console.print_json(json.dumps(result, indent=2, default=str))
@@ -12,7 +12,7 @@ import typer
12
12
  from rich.console import Console
13
13
  from rich.table import Table
14
14
 
15
- from hypercli.agents import Agent, Deployments, OpenClawAgent, DEFAULT_OPENCLAW_IMAGE
15
+ from hypercli.agents import AGENT_FILE_MAX_BYTES, Agent, Deployments, OpenClawAgent, DEFAULT_OPENCLAW_IMAGE
16
16
  from hypercli.config import get_agent_api_key as get_config_agent_api_key
17
17
 
18
18
  app = typer.Typer(help="Manage OpenClaw agent pods")
@@ -284,6 +284,8 @@ def create(
284
284
  registry_url: str = typer.Option(None, "--registry-url", help="Container registry URL for private image pulls"),
285
285
  registry_username: str = typer.Option(None, "--registry-username", help="Registry username"),
286
286
  registry_password: str = typer.Option(None, "--registry-password", help="Registry password"),
287
+ sync_uid: int = typer.Option(None, "--sync-uid", help="UID for restored synced files; defaults to Lagoon's configured value"),
288
+ sync_gid: int = typer.Option(None, "--sync-gid", help="GID for restored synced files; defaults to Lagoon's configured value"),
287
289
  gateway_token: str = typer.Option(None, "--gateway-token", help="OpenClaw gateway token override"),
288
290
  dry_run: bool = typer.Option(False, "--dry-run", help="Validate launch configuration without creating the agent or pod"),
289
291
  no_start: bool = typer.Option(False, "--no-start", help="Create without starting"),
@@ -310,6 +312,8 @@ def create(
310
312
  image=_default_openclaw_image(image),
311
313
  registry_url=registry_url,
312
314
  registry_auth=registry_auth,
315
+ sync_uid=sync_uid,
316
+ sync_gid=sync_gid,
313
317
  gateway_token=gateway_token,
314
318
  dry_run=dry_run,
315
319
  start=not no_start,
@@ -436,6 +440,36 @@ def list_agents(
436
440
 
437
441
  console.print()
438
442
  console.print(table)
443
+
444
+
445
+ @app.command("web-search")
446
+ def web_search_cmd(
447
+ query: str = typer.Argument(..., help="Search query"),
448
+ count: int = typer.Option(5, "--count", "-n", min=1, max=20, help="Number of Brave results to request"),
449
+ json_output: bool = typer.Option(False, "--json", help="Print raw JSON response"),
450
+ ):
451
+ """Search the web through the HyperClaw Brave proxy."""
452
+ agents = _get_deployments_client()
453
+ try:
454
+ payload = agents.web_search(query, count=count)
455
+ except Exception as e:
456
+ console.print(f"[red]❌ Web search failed: {e}[/red]")
457
+ raise typer.Exit(1)
458
+
459
+ if json_output:
460
+ console.print(json.dumps(payload, indent=2))
461
+ return
462
+
463
+ web = payload.get("web") if isinstance(payload, dict) else None
464
+ results = web.get("results") if isinstance(web, dict) else []
465
+ table = Table(title="Web Search")
466
+ table.add_column("Title")
467
+ table.add_column("URL")
468
+ for item in results or []:
469
+ if not isinstance(item, dict):
470
+ continue
471
+ table.add_row(str(item.get("title") or ""), str(item.get("url") or ""))
472
+ console.print(table)
439
473
  console.print()
440
474
 
441
475
 
@@ -493,6 +527,8 @@ def start(
493
527
  registry_url: str = typer.Option(None, "--registry-url", help="Container registry URL for private image pulls"),
494
528
  registry_username: str = typer.Option(None, "--registry-username", help="Registry username"),
495
529
  registry_password: str = typer.Option(None, "--registry-password", help="Registry password"),
530
+ sync_uid: int = typer.Option(None, "--sync-uid", help="UID for restored synced files; defaults to Lagoon's configured value"),
531
+ sync_gid: int = typer.Option(None, "--sync-gid", help="GID for restored synced files; defaults to Lagoon's configured value"),
496
532
  gateway_token: str = typer.Option(None, "--gateway-token", help="OpenClaw gateway token override"),
497
533
  dry_run: bool = typer.Option(False, "--dry-run", help="Validate launch configuration without starting the agent"),
498
534
  ):
@@ -521,6 +557,8 @@ def start(
521
557
  image=effective_image,
522
558
  registry_url=registry_url,
523
559
  registry_auth=registry_auth,
560
+ sync_uid=sync_uid,
561
+ sync_gid=sync_gid,
524
562
  gateway_token=effective_gateway_token,
525
563
  dry_run=dry_run,
526
564
  )
@@ -639,6 +677,9 @@ def cp(
639
677
 
640
678
  try:
641
679
  if dst_agent_id:
680
+ local_size = Path(src_path).stat().st_size
681
+ if local_size > AGENT_FILE_MAX_BYTES:
682
+ raise ValueError(f"Agent file writes are limited to {AGENT_FILE_MAX_BYTES // 1024 // 1024} MiB")
642
683
  pod = _get_pod_with_token(dst_agent_id)
643
684
  agents.cp_to(pod, src_path, dst_path)
644
685
  console.print(f"[green]✓[/green] Copied [bold]{src_path}[/bold] to [bold]{dst_agent_id[:12]}:{dst_path}[/bold]")
@@ -10,7 +10,7 @@ from rich.table import Table
10
10
  from hypercli import HyperCLI, APIError, configure
11
11
  from hypercli.config import CONFIG_FILE
12
12
 
13
- from . import agent, agents, billing, comfyui, files, flow, instances, jobs, keys, llm, user, voice, wallet
13
+ from . import agent, agents, billing, comfyui, files, flow, instances, jobs, keys, llm, memory, user, voice, wallet
14
14
  from .output import output, spinner
15
15
 
16
16
  console = Console()
@@ -94,6 +94,7 @@ app.add_typer(instances.app, name="instances")
94
94
  app.add_typer(keys.app, name="keys")
95
95
  app.add_typer(jobs.app, name="jobs")
96
96
  app.add_typer(llm.app, name="llm")
97
+ app.add_typer(memory.app, name="memory")
97
98
  app.add_typer(user.app, name="user")
98
99
  app.add_typer(voice.app, name="voice")
99
100
  app.add_typer(wallet.app, name="wallet")
@@ -192,6 +193,32 @@ def me_cmd(
192
193
  console.print(table)
193
194
 
194
195
 
196
+ @app.command("status")
197
+ def status_cmd(
198
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
199
+ ):
200
+ """Show compact platform status."""
201
+ client = HyperCLI()
202
+ with spinner("Checking platform status..."):
203
+ payload = client.status()
204
+ if fmt == "json":
205
+ output(payload, fmt)
206
+ return
207
+
208
+ table = Table(show_header=True, header_style="bold cyan")
209
+ table.add_column("Type")
210
+ table.add_column("Name")
211
+ table.add_column("Healthy")
212
+
213
+ for name, healthy in (payload.get("models") or {}).items():
214
+ table.add_row("model", str(name), "true" if healthy else "false")
215
+ for name, healthy in (payload.get("clusters") or {}).items():
216
+ table.add_row("cluster", str(name), "true" if healthy else "false")
217
+
218
+ console.print(f"ok: {'true' if payload.get('ok') else 'false'}")
219
+ console.print(table)
220
+
221
+
195
222
  @app.command("configure")
196
223
  def configure_cmd():
197
224
  """Configure HyperCLI with your API key and API URL"""
@@ -0,0 +1,334 @@
1
+ """OpenClaw memory artifact helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ import subprocess
8
+ from typing import Any
9
+
10
+ import typer
11
+ from rich.console import Console
12
+
13
+ from . import llm
14
+ from .memory_captions import (
15
+ DEFAULT_WORKSPACE,
16
+ SUPPORTED_CAPTION_EXTENSIONS,
17
+ discover_caption_files,
18
+ import_caption_file,
19
+ parse_caption_file,
20
+ transcript_lines,
21
+ )
22
+ from .memory_documents import (
23
+ SUPPORTED_DOCUMENT_EXTENSIONS,
24
+ discover_document_files,
25
+ extract_document_text,
26
+ import_document_file,
27
+ )
28
+
29
+ app = typer.Typer(help="Prepare OpenClaw memory artifacts")
30
+ console = Console()
31
+
32
+
33
+ def _split_csv(value: str | None) -> list[str] | None:
34
+ if not value:
35
+ return None
36
+ return [part.strip() for part in value.split(",") if part.strip()] or None
37
+
38
+
39
+ def _sample_transcript_text(caption_file: Path, max_chars: int = 12_000) -> str:
40
+ segments = parse_caption_file(caption_file)
41
+ lines = transcript_lines(segments)
42
+ parts: list[str] = []
43
+ for line in lines:
44
+ parts.append(line.text)
45
+ if sum(len(part) + 1 for part in parts) >= max_chars:
46
+ break
47
+ return "\n".join(parts)[:max_chars]
48
+
49
+
50
+ def _sample_document_text(document_file: Path, max_chars: int = 12_000) -> str:
51
+ return extract_document_text(document_file).text[:max_chars]
52
+
53
+
54
+ def _extract_json_object(text: str) -> dict[str, Any]:
55
+ stripped = text.strip()
56
+ if stripped.startswith("```"):
57
+ stripped = stripped.strip("`")
58
+ if stripped.lower().startswith("json"):
59
+ stripped = stripped[4:].strip()
60
+ try:
61
+ parsed = json.loads(stripped)
62
+ except json.JSONDecodeError:
63
+ start = stripped.find("{")
64
+ end = stripped.rfind("}")
65
+ if start < 0 or end <= start:
66
+ raise
67
+ parsed = json.loads(stripped[start : end + 1])
68
+ return parsed if isinstance(parsed, dict) else {}
69
+
70
+
71
+ def enrich_text_metadata(
72
+ *,
73
+ title: str,
74
+ source_kind: str,
75
+ text_sample: str,
76
+ source_url: str | None,
77
+ source_name: str | None,
78
+ model: str | None,
79
+ base_url: str | None,
80
+ key: str | None,
81
+ ) -> dict[str, Any]:
82
+ """Generate optional memory metadata through Hyper's OpenAI-compatible API."""
83
+ api_key = llm._resolve_api_key(key)
84
+ api_base = llm._resolve_api_base(base_url)
85
+ resolved_model = model or llm._resolve_default_model(api_key, api_base)
86
+ client = llm._get_openai_client(api_key, api_base)
87
+ response = client.chat.completions.create(
88
+ model=resolved_model,
89
+ stream=False,
90
+ temperature=0.2,
91
+ messages=[
92
+ {
93
+ "role": "system",
94
+ "content": (
95
+ "Return strict JSON only. Summarize extracted source text for an "
96
+ "OpenClaw memory file. Keep claims grounded in the supplied text."
97
+ ),
98
+ },
99
+ {
100
+ "role": "user",
101
+ "content": (
102
+ f"Title: {title}\n"
103
+ f"Source kind: {source_kind}\n"
104
+ f"Source name: {source_name or ''}\n"
105
+ f"Source URL: {source_url or ''}\n\n"
106
+ "Text excerpt:\n"
107
+ f"{text_sample}\n\n"
108
+ "Return JSON with keys: short_summary, long_description, keywords. "
109
+ "keywords must be a list of concise lowercase phrases."
110
+ ),
111
+ },
112
+ ],
113
+ )
114
+ content = response.choices[0].message.content or "{}"
115
+ parsed = _extract_json_object(content)
116
+ keywords = parsed.get("keywords")
117
+ if not isinstance(keywords, list):
118
+ parsed["keywords"] = []
119
+ return parsed
120
+
121
+
122
+ def enrich_caption_metadata(
123
+ *,
124
+ caption_file: Path,
125
+ title: str,
126
+ source_url: str | None,
127
+ channel: str | None,
128
+ model: str | None,
129
+ base_url: str | None,
130
+ key: str | None,
131
+ ) -> dict[str, Any]:
132
+ """Generate optional caption metadata through Hyper's OpenAI-compatible API."""
133
+ return enrich_text_metadata(
134
+ title=title,
135
+ source_kind="captions",
136
+ text_sample=_sample_transcript_text(caption_file),
137
+ source_url=source_url,
138
+ source_name=channel,
139
+ model=model,
140
+ base_url=base_url,
141
+ key=key,
142
+ )
143
+
144
+
145
+ def enrich_document_metadata(
146
+ *,
147
+ document_file: Path,
148
+ title: str,
149
+ source_url: str | None,
150
+ author: str | None,
151
+ model: str | None,
152
+ base_url: str | None,
153
+ key: str | None,
154
+ ) -> dict[str, Any]:
155
+ """Generate optional document metadata through Hyper's OpenAI-compatible API."""
156
+ return enrich_text_metadata(
157
+ title=title,
158
+ source_kind=document_file.suffix.lower().lstrip(".") or "document",
159
+ text_sample=_sample_document_text(document_file),
160
+ source_url=source_url,
161
+ source_name=author,
162
+ model=model,
163
+ base_url=base_url,
164
+ key=key,
165
+ )
166
+
167
+
168
+ def _discover_memory_files(input_path: Path) -> tuple[list[Path], list[Path]]:
169
+ if input_path.is_file():
170
+ suffix = input_path.suffix.lower()
171
+ if suffix in SUPPORTED_CAPTION_EXTENSIONS:
172
+ return [input_path], []
173
+ if suffix in SUPPORTED_DOCUMENT_EXTENSIONS:
174
+ return [], [input_path]
175
+ return [], []
176
+ return discover_caption_files(input_path), discover_document_files(input_path)
177
+
178
+
179
+ def _run_index(*, index: bool, agent: str | None) -> None:
180
+ if not index:
181
+ return
182
+ cmd = ["openclaw", "memory", "index"]
183
+ if agent:
184
+ cmd.extend(["--agent", agent])
185
+ subprocess.run(cmd, check=True)
186
+
187
+
188
+ @app.command("import")
189
+ def import_memory(
190
+ input_path: Path = typer.Argument(
191
+ ...,
192
+ help=(
193
+ "Source file or directory. Supported: .srt, .vtt, .ttml, "
194
+ ".pdf, .doc, .docx, .epub, .txt, .md"
195
+ ),
196
+ ),
197
+ workspace: Path = typer.Option(
198
+ DEFAULT_WORKSPACE,
199
+ "--workspace",
200
+ "-w",
201
+ help="OpenClaw workspace root used when --memory-dir/HYPER_MEMORY_DIR is not set",
202
+ ),
203
+ memory_dir: Path = typer.Option(
204
+ None,
205
+ "--memory-dir",
206
+ envvar="HYPER_MEMORY_DIR",
207
+ help="OpenClaw memory root; defaults to <workspace>/memory",
208
+ ),
209
+ collection: str = typer.Option(
210
+ ...,
211
+ "--collection",
212
+ "-c",
213
+ help="Collection under memory root, e.g. youtube/examplecreator",
214
+ ),
215
+ source_id: str = typer.Option(
216
+ None,
217
+ "--source-id",
218
+ help="Stable source/video id for single-file imports",
219
+ ),
220
+ title: str = typer.Option(None, "--title", help="Readable title for single-file imports"),
221
+ source_url: str = typer.Option(None, "--source-url", help="Original source URL"),
222
+ channel: str = typer.Option(None, "--channel", help="Channel/show/source name"),
223
+ channel_url: str = typer.Option(None, "--channel-url", help="Channel/show/source URL"),
224
+ author: str = typer.Option(None, "--author", help="Document author/source name"),
225
+ source_type: str = typer.Option(
226
+ None, "--source-type", help="Override document source type in frontmatter"
227
+ ),
228
+ participants: str = typer.Option(
229
+ None,
230
+ "--participants",
231
+ help="Comma-separated participant names, if known",
232
+ ),
233
+ caption_language: str = typer.Option(None, "--language", "-l", help="Caption language"),
234
+ caption_kind: str = typer.Option(None, "--caption-kind", help="manual, auto, translated, asr"),
235
+ raw_json3: Path = typer.Option(
236
+ None, "--raw-json3", help="Optional json3 file to copy/link in metadata"
237
+ ),
238
+ no_copy_raw: bool = typer.Option(
239
+ False, "--no-copy-raw", help="Do not copy raw caption/json3 files"
240
+ ),
241
+ enrich: bool = typer.Option(
242
+ False, "--enrich", help="Generate summaries/keywords through Hyper /v1"
243
+ ),
244
+ model: str = typer.Option(None, "--model", "-m", help="Model for --enrich"),
245
+ base_url: str = typer.Option(
246
+ None, "--base-url", "-b", help="Product API base URL for --enrich"
247
+ ),
248
+ key: str = typer.Option(None, "--key", "-k", help="API key for --enrich"),
249
+ index: bool = typer.Option(False, "--index", help="Run openclaw memory index after import"),
250
+ agent: str = typer.Option(None, "--agent", help="Agent id for --index"),
251
+ ):
252
+ """Import supported sources into OpenClaw-indexable Markdown memory files."""
253
+ caption_files, document_files = _discover_memory_files(input_path)
254
+ if not caption_files and not document_files:
255
+ console.print(f"[red]No supported memory source files found under {input_path}[/red]")
256
+ raise typer.Exit(1)
257
+ total_files = len(caption_files) + len(document_files)
258
+ if total_files > 1 and (source_id or title or source_url or raw_json3):
259
+ console.print(
260
+ "[red]--source-id, --title, --source-url, and --raw-json3 only apply to "
261
+ "single-file imports[/red]"
262
+ )
263
+ raise typer.Exit(1)
264
+ if raw_json3 and not caption_files:
265
+ console.print("[red]--raw-json3 only applies to caption imports[/red]")
266
+ raise typer.Exit(1)
267
+
268
+ for caption_file in caption_files:
269
+ resolved_title = title or caption_file.stem
270
+ enrichment = None
271
+ if enrich:
272
+ enrichment = enrich_caption_metadata(
273
+ caption_file=caption_file,
274
+ title=resolved_title,
275
+ source_url=source_url,
276
+ channel=channel,
277
+ model=model,
278
+ base_url=base_url,
279
+ key=key,
280
+ )
281
+ result = import_caption_file(
282
+ caption_file=caption_file,
283
+ workspace=workspace,
284
+ memory_dir=memory_dir,
285
+ collection=collection,
286
+ video_id=source_id,
287
+ title=resolved_title,
288
+ source_url=source_url,
289
+ channel=channel,
290
+ channel_url=channel_url,
291
+ participants=_split_csv(participants),
292
+ caption_language=caption_language,
293
+ caption_kind=caption_kind,
294
+ raw_json3=raw_json3,
295
+ copy_raw=not no_copy_raw,
296
+ enrichment=enrichment,
297
+ )
298
+ console.print(
299
+ f"[green]✓[/green] {result.markdown_path} "
300
+ f"({result.transcript_line_count} transcript lines)"
301
+ )
302
+
303
+ for document_file in document_files:
304
+ resolved_title = title or document_file.stem
305
+ enrichment = None
306
+ if enrich:
307
+ enrichment = enrich_document_metadata(
308
+ document_file=document_file,
309
+ title=resolved_title,
310
+ source_url=source_url,
311
+ author=author,
312
+ model=model,
313
+ base_url=base_url,
314
+ key=key,
315
+ )
316
+ result = import_document_file(
317
+ document_file=document_file,
318
+ workspace=workspace,
319
+ memory_dir=memory_dir,
320
+ collection=collection,
321
+ source_id=source_id,
322
+ title=resolved_title,
323
+ source_url=source_url,
324
+ source_type=source_type,
325
+ author=author,
326
+ copy_raw=not no_copy_raw,
327
+ enrichment=enrichment,
328
+ )
329
+ console.print(
330
+ f"[green]✓[/green] {result.markdown_path} "
331
+ f"({result.word_count} words, {result.character_count} chars)"
332
+ )
333
+
334
+ _run_index(index=index, agent=agent)