duet-cli 0.2.1__tar.gz → 0.2.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: duet-cli
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Two CLI agents in conversation. One Python file. Stdlib only.
5
5
  Author: Volkan Altan
6
6
  License-Expression: MIT
@@ -47,6 +47,38 @@ a worktree until they converge. Pass any upstream command as the kickoff:
47
47
  install that first (see below) and make sure `command -v duet` passes in Claude
48
48
  Code's shell. Full guide:
49
49
  [docs/CLAUDE_CODE_PLUGIN.md](https://github.com/volkan/duet/blob/main/docs/CLAUDE_CODE_PLUGIN.md).
50
+ If Claude Code disambiguates plugin commands with namespaces, `/duet:duet` is
51
+ the same command.
52
+
53
+ Autonomous handoff example:
54
+
55
+ ![Claude Code handoff using duet](./docs/assets/claude-duet-workflow.png?raw=true)
56
+
57
+ While the handoff runs, Claude Code shows the shell in auto mode and exposes
58
+ the exact `duet` command under shell details:
59
+
60
+ ![Claude Code auto mode shell status](./docs/assets/claude-duet-auto-mode.png?raw=true)
61
+
62
+ ![Claude Code shell details running duet](./docs/assets/claude-duet-shell-details.png?raw=true)
63
+
64
+ Copy-ready version:
65
+
66
+ ```text
67
+ /loop /goal Create a temporary todo.md from the plan above and the remaining
68
+ tasks in todo_codex.md.
69
+
70
+ 1. Use /duet:duet with max reasoning to confirm the plan.
71
+ 2. After the plan is confirmed, implement it.
72
+ 3. Once the first implementation is done, use /duet:duet with max reasoning
73
+ for code review.
74
+ 4. Use /duet:duet with max reasoning to review the second plan, then implement
75
+ it.
76
+ 5. When the process is complete and all checks are green, merge the approved
77
+ changes.
78
+
79
+ P.S. I will not be around, so handle decisions without me. If you need another
80
+ opinion, use /duet:duet to discuss it with Codex.
81
+ ```
50
82
 
51
83
  ### 2. Inside Codex — `$duet`
52
84
 
@@ -64,16 +96,26 @@ Codex's shell. Full guide:
64
96
  ### 3. From the terminal — `duet`
65
97
 
66
98
  ```bash
67
- pipx install duet-cli # the command it installs is `duet`
99
+ pipx install duet-cli # recommended; the command it installs is `duet`
68
100
  duet --task "Fix the failing test" --cwd ~/code/myrepo
69
101
  ```
70
102
 
71
- The PyPI package is `duet-cli` (bare `duet` on PyPI is Google's async library),
72
- so `pipx`/`uvx` isolation is recommended. Add `'duet-cli[yaml]'` if you want
73
- `--config foo.yaml`. One-shot, no install:
103
+ `pipx` is the recommended install. Two other persistent options put `duet` on
104
+ PATH the same way:
105
+
106
+ ```bash
107
+ uv tool install duet-cli
108
+ python3 -m pip install --user duet-cli
109
+ ```
110
+
111
+ The PyPI package is `duet-cli` (bare `duet` on PyPI is Google's async library).
112
+ Add the `[yaml]` extra for `--config foo.yaml` support — `pipx install
113
+ 'duet-cli[yaml]'`, `uv tool install 'duet-cli[yaml]'`, or `python3 -m pip
114
+ install --user 'duet-cli[yaml]'`. One-shot, no install:
74
115
  `uvx --from duet-cli duet --task "..."` — note this is ephemeral and does not put
75
116
  `duet` on PATH, so the `/duet` and `$duet` plugins need a persistent install
76
- (`pipx install duet-cli` or `make install`) instead.
117
+ (`pipx install duet-cli`, `uv tool install duet-cli`,
118
+ `python3 -m pip install --user duet-cli`, or `make install`) instead.
77
119
 
78
120
  ## Examples
79
121
 
@@ -26,6 +26,38 @@ a worktree until they converge. Pass any upstream command as the kickoff:
26
26
  install that first (see below) and make sure `command -v duet` passes in Claude
27
27
  Code's shell. Full guide:
28
28
  [docs/CLAUDE_CODE_PLUGIN.md](https://github.com/volkan/duet/blob/main/docs/CLAUDE_CODE_PLUGIN.md).
29
+ If Claude Code disambiguates plugin commands with namespaces, `/duet:duet` is
30
+ the same command.
31
+
32
+ Autonomous handoff example:
33
+
34
+ ![Claude Code handoff using duet](./docs/assets/claude-duet-workflow.png?raw=true)
35
+
36
+ While the handoff runs, Claude Code shows the shell in auto mode and exposes
37
+ the exact `duet` command under shell details:
38
+
39
+ ![Claude Code auto mode shell status](./docs/assets/claude-duet-auto-mode.png?raw=true)
40
+
41
+ ![Claude Code shell details running duet](./docs/assets/claude-duet-shell-details.png?raw=true)
42
+
43
+ Copy-ready version:
44
+
45
+ ```text
46
+ /loop /goal Create a temporary todo.md from the plan above and the remaining
47
+ tasks in todo_codex.md.
48
+
49
+ 1. Use /duet:duet with max reasoning to confirm the plan.
50
+ 2. After the plan is confirmed, implement it.
51
+ 3. Once the first implementation is done, use /duet:duet with max reasoning
52
+ for code review.
53
+ 4. Use /duet:duet with max reasoning to review the second plan, then implement
54
+ it.
55
+ 5. When the process is complete and all checks are green, merge the approved
56
+ changes.
57
+
58
+ P.S. I will not be around, so handle decisions without me. If you need another
59
+ opinion, use /duet:duet to discuss it with Codex.
60
+ ```
29
61
 
30
62
  ### 2. Inside Codex — `$duet`
31
63
 
@@ -43,16 +75,26 @@ Codex's shell. Full guide:
43
75
  ### 3. From the terminal — `duet`
44
76
 
45
77
  ```bash
46
- pipx install duet-cli # the command it installs is `duet`
78
+ pipx install duet-cli # recommended; the command it installs is `duet`
47
79
  duet --task "Fix the failing test" --cwd ~/code/myrepo
48
80
  ```
49
81
 
50
- The PyPI package is `duet-cli` (bare `duet` on PyPI is Google's async library),
51
- so `pipx`/`uvx` isolation is recommended. Add `'duet-cli[yaml]'` if you want
52
- `--config foo.yaml`. One-shot, no install:
82
+ `pipx` is the recommended install. Two other persistent options put `duet` on
83
+ PATH the same way:
84
+
85
+ ```bash
86
+ uv tool install duet-cli
87
+ python3 -m pip install --user duet-cli
88
+ ```
89
+
90
+ The PyPI package is `duet-cli` (bare `duet` on PyPI is Google's async library).
91
+ Add the `[yaml]` extra for `--config foo.yaml` support — `pipx install
92
+ 'duet-cli[yaml]'`, `uv tool install 'duet-cli[yaml]'`, or `python3 -m pip
93
+ install --user 'duet-cli[yaml]'`. One-shot, no install:
53
94
  `uvx --from duet-cli duet --task "..."` — note this is ephemeral and does not put
54
95
  `duet` on PATH, so the `/duet` and `$duet` plugins need a persistent install
55
- (`pipx install duet-cli` or `make install`) instead.
96
+ (`pipx install duet-cli`, `uv tool install duet-cli`,
97
+ `python3 -m pip install --user duet-cli`, or `make install`) instead.
56
98
 
57
99
  ## Examples
58
100
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: duet-cli
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Two CLI agents in conversation. One Python file. Stdlib only.
5
5
  Author: Volkan Altan
6
6
  License-Expression: MIT
@@ -47,6 +47,38 @@ a worktree until they converge. Pass any upstream command as the kickoff:
47
47
  install that first (see below) and make sure `command -v duet` passes in Claude
48
48
  Code's shell. Full guide:
49
49
  [docs/CLAUDE_CODE_PLUGIN.md](https://github.com/volkan/duet/blob/main/docs/CLAUDE_CODE_PLUGIN.md).
50
+ If Claude Code disambiguates plugin commands with namespaces, `/duet:duet` is
51
+ the same command.
52
+
53
+ Autonomous handoff example:
54
+
55
+ ![Claude Code handoff using duet](./docs/assets/claude-duet-workflow.png?raw=true)
56
+
57
+ While the handoff runs, Claude Code shows the shell in auto mode and exposes
58
+ the exact `duet` command under shell details:
59
+
60
+ ![Claude Code auto mode shell status](./docs/assets/claude-duet-auto-mode.png?raw=true)
61
+
62
+ ![Claude Code shell details running duet](./docs/assets/claude-duet-shell-details.png?raw=true)
63
+
64
+ Copy-ready version:
65
+
66
+ ```text
67
+ /loop /goal Create a temporary todo.md from the plan above and the remaining
68
+ tasks in todo_codex.md.
69
+
70
+ 1. Use /duet:duet with max reasoning to confirm the plan.
71
+ 2. After the plan is confirmed, implement it.
72
+ 3. Once the first implementation is done, use /duet:duet with max reasoning
73
+ for code review.
74
+ 4. Use /duet:duet with max reasoning to review the second plan, then implement
75
+ it.
76
+ 5. When the process is complete and all checks are green, merge the approved
77
+ changes.
78
+
79
+ P.S. I will not be around, so handle decisions without me. If you need another
80
+ opinion, use /duet:duet to discuss it with Codex.
81
+ ```
50
82
 
51
83
  ### 2. Inside Codex — `$duet`
52
84
 
@@ -64,16 +96,26 @@ Codex's shell. Full guide:
64
96
  ### 3. From the terminal — `duet`
65
97
 
66
98
  ```bash
67
- pipx install duet-cli # the command it installs is `duet`
99
+ pipx install duet-cli # recommended; the command it installs is `duet`
68
100
  duet --task "Fix the failing test" --cwd ~/code/myrepo
69
101
  ```
70
102
 
71
- The PyPI package is `duet-cli` (bare `duet` on PyPI is Google's async library),
72
- so `pipx`/`uvx` isolation is recommended. Add `'duet-cli[yaml]'` if you want
73
- `--config foo.yaml`. One-shot, no install:
103
+ `pipx` is the recommended install. Two other persistent options put `duet` on
104
+ PATH the same way:
105
+
106
+ ```bash
107
+ uv tool install duet-cli
108
+ python3 -m pip install --user duet-cli
109
+ ```
110
+
111
+ The PyPI package is `duet-cli` (bare `duet` on PyPI is Google's async library).
112
+ Add the `[yaml]` extra for `--config foo.yaml` support — `pipx install
113
+ 'duet-cli[yaml]'`, `uv tool install 'duet-cli[yaml]'`, or `python3 -m pip
114
+ install --user 'duet-cli[yaml]'`. One-shot, no install:
74
115
  `uvx --from duet-cli duet --task "..."` — note this is ephemeral and does not put
75
116
  `duet` on PATH, so the `/duet` and `$duet` plugins need a persistent install
76
- (`pipx install duet-cli` or `make install`) instead.
117
+ (`pipx install duet-cli`, `uv tool install duet-cli`,
118
+ `python3 -m pip install --user duet-cli`, or `make install`) instead.
77
119
 
78
120
  ## Examples
79
121
 
@@ -8,4 +8,5 @@ duet_cli.egg-info/dependency_links.txt
8
8
  duet_cli.egg-info/entry_points.txt
9
9
  duet_cli.egg-info/requires.txt
10
10
  duet_cli.egg-info/top_level.txt
11
+ tests/test_bump_release_version.py
11
12
  tests/test_duet.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "duet-cli"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  description = "Two CLI agents in conversation. One Python file. Stdlib only."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,106 @@
1
+ """Unit tests for scripts/bump_release_version.py (stdlib unittest, no subprocess)."""
2
+ import json
3
+ import sys
4
+ import tempfile
5
+ import unittest
6
+ from pathlib import Path
7
+
8
+ _REPO_ROOT = Path(__file__).resolve().parent.parent
9
+ sys.path.insert(0, str(_REPO_ROOT / "scripts"))
10
+
11
+ import bump_release_version as brv # noqa: E402
12
+
13
+ PYPROJECT = """\
14
+ [build-system]
15
+ requires = ["setuptools>=77"]
16
+ build-backend = "setuptools.build_meta"
17
+
18
+ [project]
19
+ name = "duet-cli"
20
+ version = "0.2.1"
21
+ description = "x"
22
+ """
23
+
24
+ CLAUDE_PLUGIN = {"name": "duet", "version": "0.2.1", "author": {"name": "Volkan Altan"}}
25
+ CODEX_PLUGIN = {"name": "duet", "version": "0.2.1", "skills": "./skills/"}
26
+
27
+
28
+ def _make_repo(tmp: Path) -> None:
29
+ (tmp / "pyproject.toml").write_text(PYPROJECT, encoding="utf-8")
30
+ claude = tmp / "plugins" / "duet-claude" / ".claude-plugin"
31
+ codex = tmp / "plugins" / "duet" / ".codex-plugin"
32
+ claude.mkdir(parents=True)
33
+ codex.mkdir(parents=True)
34
+ (claude / "plugin.json").write_text(json.dumps(CLAUDE_PLUGIN, indent=2) + "\n", encoding="utf-8")
35
+ (codex / "plugin.json").write_text(json.dumps(CODEX_PLUGIN, indent=2) + "\n", encoding="utf-8")
36
+
37
+
38
+ class TestParseStrictSemver(unittest.TestCase):
39
+ def test_valid(self):
40
+ self.assertEqual(brv.parse_strict_semver("1.2.3"), (1, 2, 3))
41
+ self.assertEqual(brv.parse_strict_semver("0.2.10"), (0, 2, 10))
42
+
43
+ def test_rejects(self):
44
+ for bad in ("v1.2.3", "0.3.0-rc1", "0.3.0+build", "1.2", "1.2.3.4", "1.2.x", ""):
45
+ with self.assertRaises(brv.BumpError, msg=bad):
46
+ brv.parse_strict_semver(bad)
47
+
48
+
49
+ class TestBump(unittest.TestCase):
50
+ def setUp(self):
51
+ self._tmp = tempfile.TemporaryDirectory()
52
+ self.root = Path(self._tmp.name)
53
+ _make_repo(self.root)
54
+
55
+ def tearDown(self):
56
+ self._tmp.cleanup()
57
+
58
+ def _versions(self):
59
+ py = brv.read_current_version((self.root / "pyproject.toml").read_text())
60
+ cl = json.loads((self.root / "plugins/duet-claude/.claude-plugin/plugin.json").read_text())
61
+ cx = json.loads((self.root / "plugins/duet/.codex-plugin/plugin.json").read_text())
62
+ return py, cl["version"], cx["version"]
63
+
64
+ def test_read_current_version(self):
65
+ self.assertEqual(brv.read_current_version(PYPROJECT), "0.2.1")
66
+
67
+ def test_bump_updates_all_three(self):
68
+ prev = brv.bump(self.root, "0.3.0")
69
+ self.assertEqual(prev, "0.2.1")
70
+ self.assertEqual(self._versions(), ("0.3.0", "0.3.0", "0.3.0"))
71
+
72
+ def test_bump_preserves_json_structure(self):
73
+ brv.bump(self.root, "0.2.2")
74
+ cl = json.loads((self.root / "plugins/duet-claude/.claude-plugin/plugin.json").read_text())
75
+ self.assertEqual(cl["name"], "duet")
76
+ self.assertEqual(cl["author"], {"name": "Volkan Altan"})
77
+ # only the build-system version-like string and [project] survive; the bump
78
+ # must not touch the [build-system] requires line.
79
+ py = (self.root / "pyproject.toml").read_text()
80
+ self.assertIn('requires = ["setuptools>=77"]', py)
81
+ self.assertIn('version = "0.2.2"', py)
82
+
83
+ def test_reject_downgrade(self):
84
+ with self.assertRaises(brv.BumpError):
85
+ brv.bump(self.root, "0.2.0")
86
+ self.assertEqual(self._versions(), ("0.2.1", "0.2.1", "0.2.1")) # unchanged
87
+
88
+ def test_reject_same_version(self):
89
+ with self.assertRaises(brv.BumpError):
90
+ brv.bump(self.root, "0.2.1")
91
+
92
+ def test_reject_invalid_before_write(self):
93
+ with self.assertRaises(brv.BumpError):
94
+ brv.bump(self.root, "v0.3.0")
95
+ self.assertEqual(self._versions(), ("0.2.1", "0.2.1", "0.2.1")) # no partial write
96
+
97
+ def test_missing_project_version_raises(self):
98
+ (self.root / "pyproject.toml").write_text(
99
+ "[build-system]\nrequires = []\n", encoding="utf-8"
100
+ )
101
+ with self.assertRaises(brv.BumpError):
102
+ brv.bump(self.root, "0.3.0")
103
+
104
+
105
+ if __name__ == "__main__":
106
+ unittest.main()
File without changes
File without changes
File without changes
File without changes