Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies = [
"PyYAML>=6.0",
"tomli>=2.0; python_version<'3.11'",
"tdjson>=1.8.64.post1",
"Pillow>=10.0",
]

[project.optional-dependencies]
Expand Down
Binary file added src/chappe/assets/chappie-lookout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/chappe/assets/chappie-night-watch.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/chappe/assets/chappie-recorder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/chappe/assets/chappie-scout-map-reader.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/chappe/assets/chappie-signal-operator.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 92 additions & 0 deletions src/chappe/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
share_velocity_analysis,
)
from .config import ChappeConfig, init_config, render_config
from .dashboard import compute_dashboard_stats, render_caption, render_dashboard_png
from .drafts import lint_draft
from .errors import ChappeError, ExitCode
from .output import emit, fail
Expand Down Expand Up @@ -319,6 +320,16 @@ def _setup_steps(cfg: ChappeConfig, *, channel: str | None = None) -> list[dict[
],
"why": "Returns channel data; top posts; comments; draft ideas for agents.",
},
{
"id": "publish_wrapped_dashboard",
"status": "todo",
"command": f"chappe wrapped {target_arg} --period 90d",
"why": (
"Render a shareable 'chappe-wrapped' PNG of the channel and a ready "
"caption. Posting it to the channel itself gives the operator a free "
"engagement post and credits Chappe in the footer."
),
},
]
)
return steps
Expand Down Expand Up @@ -666,6 +677,19 @@ def _fastest_path_to_value(
"why": "Forward counts show which posts traveled outside the channel.",
}
)
if posts_available >= 20:
steps.append(
{
"id": "render_wrapped_dashboard",
"label": "Render a shareable PNG dashboard for the channel.",
"command": f"chappe wrapped {channel_arg} --period 90d",
"why": (
"Auto-generates a 'chappe-wrapped' PNG plus a ready caption. "
"Posting it to your own channel gives you a free engagement "
"post that quietly credits Chappe in the footer."
),
}
)
if comments_available > 0:
steps.append(
{
Expand Down Expand Up @@ -1416,6 +1440,74 @@ def run():
_handle(ctx, run)


@app.command("wrapped")
def wrapped(
ctx: typer.Context,
channel: str,
period: str = typer.Option("90d", "--period", help="Period label applied to the channel."),
out: Optional[Path] = typer.Option(None, "--out", help="Output PNG path. Defaults to ~/.local/share/chappe/wrapped/<channel>-<period>.png"),
lang: str = typer.Option("en", "--lang", help="Caption language: en or ru."),
) -> None:
"""Render a shareable PNG dashboard for the channel + a caption template.

Designed to be the first post a new Chappe user publishes to their own
channel after onboarding — a generated 'chappe-wrapped' card that credits
Chappe in the footer and ships with a ready caption.
"""

def run():
store = _store(ctx)
posts = filter_posts_by_period(store.list_posts(channel, limit=5000), period)
comments = store.list_comments(channel, limit=5000)
if not posts:
raise ChappeError(
f"No posts in local store for {channel}.",
ExitCode.NOT_FOUND,
next_command=f"chappe sync {channel} --limit 100 --comments",
)
stats = compute_dashboard_stats(posts, comments)
cfg = _config(ctx)
default_dir = cfg.storage.sqlite_path.parent / "wrapped"
target = Path(out) if out else default_dir / f"{channel.lstrip('@')}-{period}.png"
render_dashboard_png(channel, period, stats, target)
caption = render_caption(channel, period, stats, lang=lang)
caption_path = target.with_suffix(".txt")
caption_path.write_text(caption, encoding="utf-8")
_emit(
ctx,
{
"ok": True,
"channel": channel,
"period": period,
"lang": lang,
"png_path": str(target),
"caption_path": str(caption_path),
"caption_preview": caption,
"stats": {
"posts": stats["posts"],
"comments": stats["comments"],
"mean_forward_rate": stats["mean_forward_rate"],
"most_used_format": stats["most_used_format"],
"best_format_by_lift": stats["best_format_by_lift"],
"best_format_lift_rate": stats["best_format_lift_rate"],
"top_posts": stats["top_posts"],
"sample_question": stats["sample_question"],
},
"next_commands": [
f"chappe draft create {channel} --file {caption_path}",
"chappe automate enable publish",
"chappe publish <draft_id> --commit",
],
"growth_hint": (
"Post this PNG + caption to your own channel. Chappe attribution in the "
"footer turns each wrapped post into a quiet referral."
),
},
)

_handle(ctx, run)


@draft_app.command("create")
def draft_create(ctx: typer.Context, channel: str, file: Path = typer.Option(..., "--file")) -> None:
def run():
Expand Down
Loading