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.
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/PKG-INFO +5 -4
- hypercli_cli-2026.6.21/hypercli_cli/__init__.py +1 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/agent.py +5 -1
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/agents.py +42 -1
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/cli.py +28 -1
- hypercli_cli-2026.6.21/hypercli_cli/memory.py +334 -0
- hypercli_cli-2026.6.21/hypercli_cli/memory_captions.py +417 -0
- hypercli_cli-2026.6.21/hypercli_cli/memory_documents.py +330 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/output.py +22 -4
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/pyproject.toml +5 -4
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_agent_subscribe_command.py +31 -1
- hypercli_cli-2026.6.21/tests/test_agents_budget_command.py +54 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_exec_shell_dryrun.py +28 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_keys_command.py +1 -1
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_me_command.py +56 -0
- hypercli_cli-2026.6.21/tests/test_memory_captions.py +265 -0
- hypercli_cli-2026.6.21/tests/test_memory_command.py +14 -0
- hypercli_cli-2026.6.21/tests/test_memory_documents.py +190 -0
- hypercli_cli-2026.6.21/tests/test_status_command.py +26 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_voice_command.py +9 -1
- hypercli_cli-2026.5.21/hypercli_cli/__init__.py +0 -1
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/.gitignore +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/README.md +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/billing.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/comfyui.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/embed.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/files.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/flow.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/instances.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/jobs.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/keys.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/llm.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/onboard.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/renders.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/stt.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/tui/__init__.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/tui/job_monitor.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/user.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/voice.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/hypercli_cli/wallet.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_agent_env_resolution.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_config_command.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_flow_command.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_flow_visibility.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_jobs_list_tags.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_llm_command.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_openclaw_config.py +0 -0
- {hypercli_cli-2026.5.21 → hypercli_cli-2026.6.21}/tests/test_wallet_command.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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)
|