email-cli-tool 0.3.0__tar.gz → 0.3.1__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 (33) hide show
  1. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/PKG-INFO +3 -3
  2. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/README.md +2 -2
  3. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/README_CN.md +2 -2
  4. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/pyproject.toml +1 -1
  5. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/src/emailcli/cli.py +1 -1
  6. email_cli_tool-0.3.1/src/emailcli/skill_install.py +71 -0
  7. email_cli_tool-0.3.1/src/emailcli/skills/wait-email/SKILL.md +60 -0
  8. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/tests/test_skill_install.py +29 -14
  9. email_cli_tool-0.3.0/src/emailcli/skill_install.py +0 -65
  10. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/.github/workflows/publish.yml +0 -0
  11. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/.github/workflows/test.yml +0 -0
  12. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/.gitignore +0 -0
  13. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/.python-version +0 -0
  14. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/LICENSE +0 -0
  15. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/docs/superpowers/plans/2026-03-25-emailcli.md +0 -0
  16. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/docs/superpowers/plans/2026-06-10-emailcli-skill-install.md +0 -0
  17. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/docs/superpowers/specs/2026-03-25-emailcli-design.md +0 -0
  18. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/docs/superpowers/specs/2026-06-10-emailcli-skill-install-design.md +0 -0
  19. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/src/emailcli/__init__.py +0 -0
  20. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/src/emailcli/config.py +0 -0
  21. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/src/emailcli/exceptions.py +0 -0
  22. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/src/emailcli/message.py +0 -0
  23. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/src/emailcli/py.typed +0 -0
  24. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/src/emailcli/receiver.py +0 -0
  25. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/src/emailcli/sender.py +0 -0
  26. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/src/emailcli/skills/send-email/SKILL.md +0 -0
  27. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/tests/__init__.py +0 -0
  28. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/tests/test_cli.py +0 -0
  29. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/tests/test_config.py +0 -0
  30. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/tests/test_message.py +0 -0
  31. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/tests/test_receiver.py +0 -0
  32. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/tests/test_sender.py +0 -0
  33. {email_cli_tool-0.3.0 → email_cli_tool-0.3.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: email-cli-tool
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: A simple CLI tool for sending emails with plain text, HTML, and file attachments
5
5
  Project-URL: Homepage, https://github.com/ClaymanTwinkle/email-cli-tool
6
6
  Project-URL: Repository, https://github.com/ClaymanTwinkle/email-cli-tool
@@ -158,7 +158,7 @@ Display current configuration (password is masked).
158
158
 
159
159
  ### `emailcli skill install`
160
160
 
161
- Install the `send-email` agent skill so Claude Code or Codex can send mail for you.
161
+ Install the `send-email` and `wait-email` agent skills so Claude Code or Codex can send and receive mail for you.
162
162
 
163
163
  ```bash
164
164
  # Install for both Claude Code and Codex (default)
@@ -169,7 +169,7 @@ emailcli skill install --target claude
169
169
  emailcli skill install --target codex
170
170
  ```
171
171
 
172
- Writes `SKILL.md` to `~/.claude/skills/send-email/` and/or `~/.codex/skills/send-email/`.
172
+ Writes each skill's `SKILL.md` under `~/.claude/skills/` and/or `~/.codex/skills/`.
173
173
 
174
174
  ## Configuration
175
175
 
@@ -132,7 +132,7 @@ Display current configuration (password is masked).
132
132
 
133
133
  ### `emailcli skill install`
134
134
 
135
- Install the `send-email` agent skill so Claude Code or Codex can send mail for you.
135
+ Install the `send-email` and `wait-email` agent skills so Claude Code or Codex can send and receive mail for you.
136
136
 
137
137
  ```bash
138
138
  # Install for both Claude Code and Codex (default)
@@ -143,7 +143,7 @@ emailcli skill install --target claude
143
143
  emailcli skill install --target codex
144
144
  ```
145
145
 
146
- Writes `SKILL.md` to `~/.claude/skills/send-email/` and/or `~/.codex/skills/send-email/`.
146
+ Writes each skill's `SKILL.md` under `~/.claude/skills/` and/or `~/.codex/skills/`.
147
147
 
148
148
  ## Configuration
149
149
 
@@ -132,7 +132,7 @@ emailcli watch --timeout 300 --save-attachments ./downloads
132
132
 
133
133
  ### `emailcli skill install`
134
134
 
135
- 安装 `send-email` 技能,让 Claude Code 或 Codex 可以直接帮你发邮件。
135
+ 安装 `send-email` 和 `wait-email` 技能,让 Claude Code 或 Codex 可以直接帮你收发邮件。
136
136
 
137
137
  ```bash
138
138
  # 默认同时安装到 Claude Code 和 Codex
@@ -143,7 +143,7 @@ emailcli skill install --target claude
143
143
  emailcli skill install --target codex
144
144
  ```
145
145
 
146
- 会把 `SKILL.md` 写入 `~/.claude/skills/send-email/` 和/或 `~/.codex/skills/send-email/`。
146
+ 会把各技能的 `SKILL.md` 写入 `~/.claude/skills/` 和/或 `~/.codex/skills/` 下的同名目录。
147
147
 
148
148
  ## 配置文件
149
149
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "email-cli-tool"
3
- version = "0.3.0"
3
+ version = "0.3.1"
4
4
  description = "A simple CLI tool for sending emails with plain text, HTML, and file attachments"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -252,7 +252,7 @@ def skill_group():
252
252
  )
253
253
  @click.option("--home", default=None, type=click.Path(), hidden=True, help="Home directory (for testing).")
254
254
  def install(target, home):
255
- """Install the send-email skill into Claude Code and/or Codex."""
255
+ """Install the emailcli agent skills into Claude Code and/or Codex."""
256
256
  home_dir = Path(home) if home else Path.home()
257
257
  targets = ["claude", "codex"] if target == "all" else [target]
258
258
 
@@ -0,0 +1,71 @@
1
+ from dataclasses import dataclass
2
+ from importlib.resources import files
3
+ from pathlib import Path
4
+ from typing import Literal
5
+
6
+ SKILL_NAMES = ["send-email", "wait-email"]
7
+
8
+ # target name -> home subdirectory that holds the agent's skills
9
+ TARGET_HOME_DIRS = {
10
+ "claude": ".claude",
11
+ "codex": ".codex",
12
+ }
13
+
14
+ # human-facing label per target
15
+ TARGET_LABELS = {
16
+ "claude": "Claude",
17
+ "codex": "Codex",
18
+ }
19
+
20
+
21
+ def load_skill_content(skill_name: str) -> str:
22
+ """Read a bundled SKILL.md shipped as package data."""
23
+ return (
24
+ files("emailcli")
25
+ .joinpath(f"skills/{skill_name}/SKILL.md")
26
+ .read_text(encoding="utf-8")
27
+ )
28
+
29
+
30
+ def skill_dest(home: Path, target: str, skill_name: str) -> Path:
31
+ """Destination SKILL.md path for a target under the given home dir."""
32
+ return home / TARGET_HOME_DIRS[target] / "skills" / skill_name / "SKILL.md"
33
+
34
+
35
+ @dataclass
36
+ class InstallResult:
37
+ target: str
38
+ skill: str
39
+ path: Path
40
+ status: Literal["created", "updated", "failed"]
41
+ error: str | None = None
42
+
43
+
44
+ def install_skill(home: Path, targets: list[str]) -> list[InstallResult]:
45
+ """Write the bundled skills into each target's user-level skills dir.
46
+
47
+ Each (target, skill) pair is handled independently; a write failure on
48
+ one does not stop the others.
49
+ """
50
+ unknown = [t for t in targets if t not in TARGET_HOME_DIRS]
51
+ if unknown:
52
+ raise ValueError(f"Unknown target(s): {', '.join(unknown)}")
53
+ contents = {name: load_skill_content(name) for name in SKILL_NAMES}
54
+ results: list[InstallResult] = []
55
+ for target in targets:
56
+ for skill_name in SKILL_NAMES:
57
+ dest = skill_dest(home, target, skill_name)
58
+ try:
59
+ existed = dest.exists()
60
+ dest.parent.mkdir(parents=True, exist_ok=True)
61
+ dest.write_text(contents[skill_name], encoding="utf-8")
62
+ results.append(
63
+ InstallResult(
64
+ target, skill_name, dest, "updated" if existed else "created"
65
+ )
66
+ )
67
+ except OSError as exc:
68
+ results.append(
69
+ InstallResult(target, skill_name, dest, "failed", str(exc))
70
+ )
71
+ return results
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: wait-email
3
+ description: "Wait for the next incoming email via emailcli. Prints sender/subject/body and downloads attachments. Use when the user wants to wait for, receive, or read an incoming email."
4
+ ---
5
+
6
+ # Wait for Email
7
+
8
+ Wait for the next incoming email from Claude Code using the `emailcli watch` command.
9
+
10
+ ## Usage
11
+
12
+ Invoke with `/wait-email` followed by natural language describing what to wait for:
13
+
14
+ ```
15
+ /wait-email 等下一封新邮件
16
+ /wait-email 等 5 分钟,附件存到 ./downloads
17
+ /wait-email 等一封验证码邮件,把验证码读出来
18
+ ```
19
+
20
+ ## Workflow
21
+
22
+ ### 1. Parse user intent
23
+
24
+ Extract from the user's message:
25
+ - **Timeout** (`--timeout`): seconds to wait. Default to 300 if the user gives no limit; use 0 (wait forever) only if the user explicitly asks to wait indefinitely.
26
+ - **Attachment directory** (`--save-attachments`): where to save attachments, if the user mentions them.
27
+ - **Mailbox** (`--mailbox`): defaults to INBOX.
28
+
29
+ ### 2. Run the command
30
+
31
+ ```bash
32
+ emailcli watch --timeout 300 --save-attachments ./downloads
33
+ ```
34
+
35
+ - The command blocks until an email arrives, so set the Bash tool timeout comfortably larger than `--timeout` (or run it in the background for long waits).
36
+ - Headers and body go to stdout; progress messages and saved-attachment paths go to stderr.
37
+ - Only emails arriving **after** the command starts are matched; existing unread mail is ignored.
38
+
39
+ ### 3. Report result
40
+
41
+ By exit code:
42
+ - **0** — email received: report From / Subject / Date, the body (or the specific detail the user asked for, e.g. a verification code), and any saved attachment paths.
43
+ - **2** — timed out with no new email: tell the user.
44
+ - **1** — error: show the message. If it says IMAP settings are missing, tell the user to re-run `emailcli init` or add an `imap:` section to `~/.emailcli/config.yaml`.
45
+
46
+ ## Command Reference
47
+
48
+ ```bash
49
+ emailcli watch \
50
+ [--timeout SECONDS] # 0 = wait forever
51
+ [--poll-interval SECONDS] # default: 10
52
+ [--save-attachments DIR] \
53
+ [--mailbox NAME] # default: INBOX
54
+ ```
55
+
56
+ ## Important
57
+
58
+ - Requires email-cli-tool **0.3.0+** with IMAP configured (`emailcli init`).
59
+ - The received email is marked as read on the server.
60
+ - Do NOT ask for confirmation — start waiting once inputs are clear, and tell the user you are waiting.
@@ -5,26 +5,38 @@ import pytest
5
5
  from click.testing import CliRunner
6
6
 
7
7
  from emailcli.cli import cli
8
- from emailcli.skill_install import InstallResult, install_skill, load_skill_content
8
+ from emailcli.skill_install import (
9
+ SKILL_NAMES,
10
+ InstallResult,
11
+ install_skill,
12
+ load_skill_content,
13
+ )
9
14
 
10
15
 
11
- def test_bundled_skill_md_is_packaged():
16
+ def test_bundled_send_email_skill_is_packaged():
12
17
  content = files("emailcli").joinpath("skills/send-email/SKILL.md").read_text(encoding="utf-8")
13
18
  assert content.startswith("---")
14
19
  assert "name: send-email" in content
15
20
  assert "emailcli send" in content
16
21
 
17
22
 
23
+ def test_bundled_wait_email_skill_is_packaged():
24
+ content = files("emailcli").joinpath("skills/wait-email/SKILL.md").read_text(encoding="utf-8")
25
+ assert content.startswith("---")
26
+ assert "name: wait-email" in content
27
+ assert "emailcli watch" in content
28
+
29
+
18
30
  def test_install_all_writes_both_targets(tmp_path):
19
31
  results = install_skill(tmp_path, ["claude", "codex"])
20
32
 
21
- claude_path = tmp_path / ".claude" / "skills" / "send-email" / "SKILL.md"
22
- codex_path = tmp_path / ".codex" / "skills" / "send-email" / "SKILL.md"
23
- bundled = load_skill_content()
24
-
25
- assert claude_path.read_text(encoding="utf-8") == bundled
26
- assert codex_path.read_text(encoding="utf-8") == bundled
27
- assert {r.target for r in results} == {"claude", "codex"}
33
+ for target_dir in (".claude", ".codex"):
34
+ for skill in SKILL_NAMES:
35
+ path = tmp_path / target_dir / "skills" / skill / "SKILL.md"
36
+ assert path.read_text(encoding="utf-8") == load_skill_content(skill)
37
+ assert {(r.target, r.skill) for r in results} == {
38
+ (t, s) for t in ("claude", "codex") for s in SKILL_NAMES
39
+ }
28
40
  assert all(r.status == "created" for r in results)
29
41
 
30
42
 
@@ -32,6 +44,7 @@ def test_install_claude_only(tmp_path):
32
44
  install_skill(tmp_path, ["claude"])
33
45
 
34
46
  assert (tmp_path / ".claude" / "skills" / "send-email" / "SKILL.md").exists()
47
+ assert (tmp_path / ".claude" / "skills" / "wait-email" / "SKILL.md").exists()
35
48
  assert not (tmp_path / ".codex").exists()
36
49
 
37
50
 
@@ -39,9 +52,9 @@ def test_install_is_idempotent(tmp_path):
39
52
  install_skill(tmp_path, ["claude"])
40
53
  results = install_skill(tmp_path, ["claude"])
41
54
 
42
- assert results[0].status == "updated"
55
+ assert all(r.status == "updated" for r in results)
43
56
  path = tmp_path / ".claude" / "skills" / "send-email" / "SKILL.md"
44
- assert path.read_text(encoding="utf-8") == load_skill_content()
57
+ assert path.read_text(encoding="utf-8") == load_skill_content("send-email")
45
58
 
46
59
 
47
60
  def test_install_unknown_target_raises(tmp_path):
@@ -54,8 +67,9 @@ def test_cli_skill_install_default_writes_both(tmp_path):
54
67
  result = runner.invoke(cli, ["skill", "install", "--home", str(tmp_path)])
55
68
 
56
69
  assert result.exit_code == 0
57
- assert (tmp_path / ".claude" / "skills" / "send-email" / "SKILL.md").exists()
58
- assert (tmp_path / ".codex" / "skills" / "send-email" / "SKILL.md").exists()
70
+ for target_dir in (".claude", ".codex"):
71
+ for skill in SKILL_NAMES:
72
+ assert (tmp_path / target_dir / "skills" / skill / "SKILL.md").exists()
59
73
  assert "Claude" in result.output
60
74
  assert "Codex" in result.output
61
75
 
@@ -68,6 +82,7 @@ def test_cli_skill_install_target_codex(tmp_path):
68
82
 
69
83
  assert result.exit_code == 0
70
84
  assert (tmp_path / ".codex" / "skills" / "send-email" / "SKILL.md").exists()
85
+ assert (tmp_path / ".codex" / "skills" / "wait-email" / "SKILL.md").exists()
71
86
  assert not (tmp_path / ".claude").exists()
72
87
 
73
88
 
@@ -81,7 +96,7 @@ def test_cli_skill_install_invalid_target(tmp_path):
81
96
 
82
97
 
83
98
  def test_cli_skill_install_failure_exit_code(tmp_path):
84
- failed = [InstallResult("claude", tmp_path / "SKILL.md", "failed", "boom")]
99
+ failed = [InstallResult("claude", "send-email", tmp_path / "SKILL.md", "failed", "boom")]
85
100
  runner = CliRunner()
86
101
  with patch("emailcli.skill_install.install_skill", return_value=failed):
87
102
  result = runner.invoke(cli, ["skill", "install", "--home", str(tmp_path)])
@@ -1,65 +0,0 @@
1
- from dataclasses import dataclass
2
- from importlib.resources import files
3
- from pathlib import Path
4
- from typing import Literal
5
-
6
- SKILL_NAME = "send-email"
7
-
8
- # target name -> home subdirectory that holds the agent's skills
9
- TARGET_HOME_DIRS = {
10
- "claude": ".claude",
11
- "codex": ".codex",
12
- }
13
-
14
- # human-facing label per target
15
- TARGET_LABELS = {
16
- "claude": "Claude",
17
- "codex": "Codex",
18
- }
19
-
20
-
21
- def load_skill_content() -> str:
22
- """Read the bundled SKILL.md shipped as package data."""
23
- return (
24
- files("emailcli")
25
- .joinpath(f"skills/{SKILL_NAME}/SKILL.md")
26
- .read_text(encoding="utf-8")
27
- )
28
-
29
-
30
- def skill_dest(home: Path, target: str) -> Path:
31
- """Destination SKILL.md path for a target under the given home dir."""
32
- return home / TARGET_HOME_DIRS[target] / "skills" / SKILL_NAME / "SKILL.md"
33
-
34
-
35
- @dataclass
36
- class InstallResult:
37
- target: str
38
- path: Path
39
- status: Literal["created", "updated", "failed"]
40
- error: str | None = None
41
-
42
-
43
- def install_skill(home: Path, targets: list[str]) -> list[InstallResult]:
44
- """Write the bundled skill into each target's user-level skills dir.
45
-
46
- Each target is handled independently; a write failure on one target is
47
- recorded and does not stop the others.
48
- """
49
- unknown = [t for t in targets if t not in TARGET_HOME_DIRS]
50
- if unknown:
51
- raise ValueError(f"Unknown target(s): {', '.join(unknown)}")
52
- content = load_skill_content()
53
- results: list[InstallResult] = []
54
- for target in targets:
55
- dest = skill_dest(home, target)
56
- try:
57
- existed = dest.exists()
58
- dest.parent.mkdir(parents=True, exist_ok=True)
59
- dest.write_text(content, encoding="utf-8")
60
- results.append(
61
- InstallResult(target, dest, "updated" if existed else "created")
62
- )
63
- except OSError as exc:
64
- results.append(InstallResult(target, dest, "failed", str(exc)))
65
- return results
File without changes
File without changes