tloop-cli 0.4.0__tar.gz → 0.5.0__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.
- {tloop_cli-0.4.0/src/tloop_cli.egg-info → tloop_cli-0.5.0}/PKG-INFO +3 -3
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/README.md +2 -2
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/pyproject.toml +1 -1
- tloop_cli-0.5.0/src/cmd_edit.py +108 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/cmd_run.py +1 -2
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/config.py +2 -28
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/runner/__init__.py +1 -1
- tloop_cli-0.5.0/src/runner/cybervisor.py +35 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/task.py +8 -8
- {tloop_cli-0.4.0 → tloop_cli-0.5.0/src/tloop_cli.egg-info}/PKG-INFO +3 -3
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/test/test_cli.py +61 -5
- tloop_cli-0.4.0/src/cmd_edit.py +0 -45
- tloop_cli-0.4.0/src/runner/cybervisor.py +0 -28
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/LICENSE +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/setup.cfg +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/claude_runner.py +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/cmd_archive.py +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/cmd_migrate.py +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/git_ops.py +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/main.py +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/runner/claude.py +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/state.py +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/tloop_cli.egg-info/SOURCES.txt +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/tloop_cli.egg-info/dependency_links.txt +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/tloop_cli.egg-info/entry_points.txt +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/tloop_cli.egg-info/requires.txt +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/src/tloop_cli.egg-info/top_level.txt +0 -0
- {tloop_cli-0.4.0 → tloop_cli-0.5.0}/test/test_git_ops.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tloop-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Automated Claude Code task runner
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -16,8 +16,8 @@ Dynamic: license-file
|
|
|
16
16
|
## 安装
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
pip install
|
|
20
|
-
pip install -e .
|
|
19
|
+
pip install tloop-cli # 从 PyPI 安装
|
|
20
|
+
pip install -e . # 本地开发安装
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
需要 Python >=3.9,唯一外部依赖:`pyyaml`。
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""tloop edit — open ~/.tloop/tasks.yaml in editor."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
import config
|
|
10
|
+
|
|
11
|
+
KNOWN_EDITORS = {
|
|
12
|
+
"code": ("Visual Studio Code", "code"),
|
|
13
|
+
"vim": ("Vim", "vim"),
|
|
14
|
+
"nano": ("Nano", "nano"),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
EDIT_HELP = """\
|
|
18
|
+
Open ~/.tloop/tasks.yaml in your editor.
|
|
19
|
+
|
|
20
|
+
On first run, you will be prompted to choose an editor (VS Code, Vim, Nano,
|
|
21
|
+
or a custom command). The choice is saved to ~/.tloop/settings.json.
|
|
22
|
+
Override anytime with: tloop edit --editor <command>
|
|
23
|
+
|
|
24
|
+
Task file format (~/.tloop/tasks.yaml):
|
|
25
|
+
|
|
26
|
+
tasks:
|
|
27
|
+
- name: My task
|
|
28
|
+
dir: ~/projects/my-project
|
|
29
|
+
prompt: |
|
|
30
|
+
Describe what Claude should do.
|
|
31
|
+
# OR:
|
|
32
|
+
prompt_file: ./prompts/my-task.md
|
|
33
|
+
branch: true # true=auto, "custom/name", false=skip
|
|
34
|
+
|
|
35
|
+
Each task runs in the specified directory. Completed tasks are
|
|
36
|
+
archived to ~/.tloop/archive/ after each run cycle.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _load_settings():
|
|
41
|
+
if config.SETTINGS_FILE.exists():
|
|
42
|
+
try:
|
|
43
|
+
return json.loads(config.SETTINGS_FILE.read_text())
|
|
44
|
+
except (json.JSONDecodeError, OSError):
|
|
45
|
+
pass
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _save_settings(settings):
|
|
50
|
+
config.TLOOP_HOME.mkdir(exist_ok=True)
|
|
51
|
+
config.SETTINGS_FILE.write_text(json.dumps(settings, indent=2) + "\n")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _prompt_editor():
|
|
55
|
+
options = []
|
|
56
|
+
for key, (label, cmd) in KNOWN_EDITORS.items():
|
|
57
|
+
if shutil.which(cmd):
|
|
58
|
+
options.append((key, label, cmd))
|
|
59
|
+
|
|
60
|
+
print(f"{config.BOLD}Choose your editor for tasks.yaml:{config.RESET}\n")
|
|
61
|
+
for i, (key, label, _) in enumerate(options, 1):
|
|
62
|
+
print(f" {i}) {label}")
|
|
63
|
+
print(f" {len(options) + 1}) Other (enter command manually)")
|
|
64
|
+
print()
|
|
65
|
+
|
|
66
|
+
while True:
|
|
67
|
+
choice = input(f"Enter number [1-{len(options) + 1}]: ").strip()
|
|
68
|
+
if choice.isdigit():
|
|
69
|
+
idx = int(choice)
|
|
70
|
+
if 1 <= idx <= len(options):
|
|
71
|
+
return options[idx - 1][2]
|
|
72
|
+
if idx == len(options) + 1:
|
|
73
|
+
cmd = input("Enter editor command: ").strip()
|
|
74
|
+
if cmd:
|
|
75
|
+
return cmd
|
|
76
|
+
print("Invalid choice, try again.")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _resolve_editor(cli_editor=None):
|
|
80
|
+
if cli_editor:
|
|
81
|
+
return cli_editor
|
|
82
|
+
settings = _load_settings()
|
|
83
|
+
if "editor" in settings:
|
|
84
|
+
return settings["editor"]
|
|
85
|
+
editor = _prompt_editor()
|
|
86
|
+
_save_settings({"editor": editor})
|
|
87
|
+
print(f"{config.GREEN}Editor saved. Change anytime with: tloop edit --editor <command>{config.RESET}\n")
|
|
88
|
+
return editor
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def add_parser(subparsers):
|
|
92
|
+
p = subparsers.add_parser(
|
|
93
|
+
"edit",
|
|
94
|
+
help="Open ~/.tloop/tasks.yaml in editor",
|
|
95
|
+
description=EDIT_HELP,
|
|
96
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
97
|
+
)
|
|
98
|
+
p.add_argument("--editor", help="Override editor command for this session")
|
|
99
|
+
p.set_defaults(func=handle)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def handle(args):
|
|
103
|
+
config.TLOOP_HOME.mkdir(exist_ok=True)
|
|
104
|
+
if not config.TASKS_FILE.exists():
|
|
105
|
+
config.TASKS_FILE.write_text(config.SAMPLE_TASKS_YAML)
|
|
106
|
+
|
|
107
|
+
editor = _resolve_editor(getattr(args, "editor", None))
|
|
108
|
+
subprocess.run([editor, str(config.TASKS_FILE)])
|
|
@@ -24,7 +24,6 @@ def handle(args):
|
|
|
24
24
|
|
|
25
25
|
cfg = load_config()
|
|
26
26
|
tasks = cfg.get("tasks", [])
|
|
27
|
-
defaults = cfg.get("defaults", {})
|
|
28
27
|
|
|
29
28
|
if not tasks:
|
|
30
29
|
print("No tasks defined in tasks.yaml")
|
|
@@ -71,7 +70,7 @@ def handle(args):
|
|
|
71
70
|
continue
|
|
72
71
|
|
|
73
72
|
ran_any = True
|
|
74
|
-
success = run_task(tasks[i], i, state
|
|
73
|
+
success = run_task(tasks[i], i, state)
|
|
75
74
|
if not success and not args.continue_on_fail:
|
|
76
75
|
print(f"\n{config.YELLOW}Stopped. Use -c to continue after failures.{config.RESET}")
|
|
77
76
|
break
|
|
@@ -8,6 +8,7 @@ import yaml
|
|
|
8
8
|
TLOOP_HOME = Path.home() / ".tloop"
|
|
9
9
|
TASKS_FILE = TLOOP_HOME / "tasks.yaml"
|
|
10
10
|
STATE_FILE = TLOOP_HOME / "state.json"
|
|
11
|
+
SETTINGS_FILE = TLOOP_HOME / "settings.json"
|
|
11
12
|
LOGS_DIR = TLOOP_HOME / "logs"
|
|
12
13
|
ARCHIVE_DIR = TLOOP_HOME / "archive"
|
|
13
14
|
|
|
@@ -18,34 +19,7 @@ CYAN = "\033[96m"
|
|
|
18
19
|
BOLD = "\033[1m"
|
|
19
20
|
RESET = "\033[0m"
|
|
20
21
|
|
|
21
|
-
SAMPLE_TASKS_YAML = ""
|
|
22
|
-
# t-loop tasks.yaml
|
|
23
|
-
# Define your Claude Code automation tasks here.
|
|
24
|
-
#
|
|
25
|
-
# Location: ~/.tloop/tasks.yaml
|
|
26
|
-
#
|
|
27
|
-
# Each task runs "cybervisor run <prompt>" in the specified directory.
|
|
28
|
-
# Completed tasks are automatically archived after each run cycle.
|
|
29
|
-
# View archives with: tloop archive
|
|
30
|
-
#
|
|
31
|
-
# prompt_file paths resolve in this order:
|
|
32
|
-
# 1. Absolute path (after ~ expansion)
|
|
33
|
-
# 2. Relative to ~/.tloop/
|
|
34
|
-
# 3. Relative to the task's dir
|
|
35
|
-
|
|
36
|
-
defaults:
|
|
37
|
-
# model: opus
|
|
38
|
-
|
|
39
|
-
tasks: []
|
|
40
|
-
# - name: My first task
|
|
41
|
-
# dir: ~/projects/my-project
|
|
42
|
-
# prompt: |
|
|
43
|
-
# Describe what Claude should do here.
|
|
44
|
-
#
|
|
45
|
-
# - name: Task with prompt file
|
|
46
|
-
# dir: ~/projects/my-project
|
|
47
|
-
# prompt_file: ./prompts/my-task.md
|
|
48
|
-
"""
|
|
22
|
+
SAMPLE_TASKS_YAML = "# Run 'tloop edit --help' for details on how to write this file.\ntasks: []\n"
|
|
49
23
|
|
|
50
24
|
|
|
51
25
|
def ensure_tloop_home():
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Cybervisor runner backend."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from runner import Runner
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CybervisorRunner(Runner):
|
|
9
|
+
def run(self, prompt, cwd, log_file=None, prompt_file=None):
|
|
10
|
+
cmd = ["cybervisor", "run"]
|
|
11
|
+
|
|
12
|
+
with open(log_file, "a") if log_file else open("/dev/null", "a") as log:
|
|
13
|
+
stdin_fh = open(prompt_file, "r") if prompt_file else None
|
|
14
|
+
try:
|
|
15
|
+
process = subprocess.Popen(
|
|
16
|
+
cmd,
|
|
17
|
+
cwd=cwd,
|
|
18
|
+
stdin=stdin_fh if stdin_fh else subprocess.PIPE,
|
|
19
|
+
stdout=subprocess.PIPE,
|
|
20
|
+
stderr=subprocess.STDOUT,
|
|
21
|
+
text=True,
|
|
22
|
+
)
|
|
23
|
+
if not stdin_fh:
|
|
24
|
+
process.stdin.write(prompt)
|
|
25
|
+
process.stdin.close()
|
|
26
|
+
for line in process.stdout:
|
|
27
|
+
print(line, end="")
|
|
28
|
+
log.write(line)
|
|
29
|
+
log.flush()
|
|
30
|
+
process.wait()
|
|
31
|
+
finally:
|
|
32
|
+
if stdin_fh:
|
|
33
|
+
stdin_fh.close()
|
|
34
|
+
|
|
35
|
+
return process.returncode
|
|
@@ -28,12 +28,11 @@ def resolve_prompt_file(prompt_file, dir_path):
|
|
|
28
28
|
return Path(prompt_file)
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def run_task(task, index, state
|
|
31
|
+
def run_task(task, index, state):
|
|
32
32
|
name = task.get("name", f"Task {index + 1}")
|
|
33
|
-
dir_path = expand_dir(task.get("dir",
|
|
33
|
+
dir_path = expand_dir(task.get("dir", "."))
|
|
34
34
|
prompt = task.get("prompt", "")
|
|
35
35
|
prompt_file = task.get("prompt_file")
|
|
36
|
-
model = task.get("model", defaults.get("model"))
|
|
37
36
|
branch_config = task.get("branch", True)
|
|
38
37
|
|
|
39
38
|
if not os.path.isdir(dir_path):
|
|
@@ -46,10 +45,11 @@ def run_task(task, index, state, defaults):
|
|
|
46
45
|
save_state(state)
|
|
47
46
|
return False
|
|
48
47
|
|
|
48
|
+
resolved_pf = None
|
|
49
49
|
if prompt_file:
|
|
50
|
-
|
|
51
|
-
if
|
|
52
|
-
prompt =
|
|
50
|
+
resolved_pf = resolve_prompt_file(prompt_file, dir_path)
|
|
51
|
+
if resolved_pf.exists():
|
|
52
|
+
prompt = resolved_pf.read_text()
|
|
53
53
|
else:
|
|
54
54
|
print(f"{config.RED} Prompt file not found: {prompt_file}{config.RESET}")
|
|
55
55
|
return False
|
|
@@ -100,12 +100,12 @@ def run_task(task, index, state, defaults):
|
|
|
100
100
|
try:
|
|
101
101
|
with open(log_file, "a") as log:
|
|
102
102
|
log.write(f"Started: {started}\n")
|
|
103
|
-
log.write(f"Command: cybervisor run <prompt
|
|
103
|
+
log.write(f"Command: cybervisor run < {'<prompt_file>' if resolved_pf else '<prompt>'}\n")
|
|
104
104
|
log.write("-" * 60 + "\n\n")
|
|
105
105
|
log.flush()
|
|
106
106
|
|
|
107
107
|
runner = CybervisorRunner()
|
|
108
|
-
returncode = runner.run(prompt, dir_path,
|
|
108
|
+
returncode = runner.run(prompt, dir_path, log_file=log_file, prompt_file=resolved_pf)
|
|
109
109
|
|
|
110
110
|
if returncode == 0:
|
|
111
111
|
state["tasks"][str(index)] = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tloop-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Automated Claude Code task runner
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -16,8 +16,8 @@ Dynamic: license-file
|
|
|
16
16
|
## 安装
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
pip install
|
|
20
|
-
pip install -e .
|
|
19
|
+
pip install tloop-cli # 从 PyPI 安装
|
|
20
|
+
pip install -e . # 本地开发安装
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
需要 Python >=3.9,唯一外部依赖:`pyyaml`。
|
|
@@ -15,6 +15,7 @@ import config
|
|
|
15
15
|
import state as state_mod
|
|
16
16
|
import task as task_mod
|
|
17
17
|
import cmd_run
|
|
18
|
+
import cmd_edit
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def _init_git_repo(tmpdir):
|
|
@@ -44,6 +45,7 @@ def _setup_home(tmp_path):
|
|
|
44
45
|
patch.object(config, "TLOOP_HOME", home),
|
|
45
46
|
patch.object(config, "TASKS_FILE", home / "tasks.yaml"),
|
|
46
47
|
patch.object(config, "STATE_FILE", home / "state.json"),
|
|
48
|
+
patch.object(config, "SETTINGS_FILE", home / "settings.json"),
|
|
47
49
|
patch.object(config, "LOGS_DIR", home / "logs"),
|
|
48
50
|
patch.object(config, "ARCHIVE_DIR", home / "archive"),
|
|
49
51
|
]
|
|
@@ -60,6 +62,7 @@ class EnsureTloopHomeTests(unittest.TestCase):
|
|
|
60
62
|
patch.object(config, "TLOOP_HOME", self.home),
|
|
61
63
|
patch.object(config, "TASKS_FILE", self.home / "tasks.yaml"),
|
|
62
64
|
patch.object(config, "STATE_FILE", self.home / "state.json"),
|
|
65
|
+
patch.object(config, "SETTINGS_FILE", self.home / "settings.json"),
|
|
63
66
|
patch.object(config, "LOGS_DIR", self.home / "logs"),
|
|
64
67
|
patch.object(config, "ARCHIVE_DIR", self.home / "archive"),
|
|
65
68
|
]
|
|
@@ -86,7 +89,7 @@ class EnsureTloopHomeTests(unittest.TestCase):
|
|
|
86
89
|
self.assertTrue(tasks_file.exists())
|
|
87
90
|
content = tasks_file.read_text()
|
|
88
91
|
self.assertIn("tasks:", content)
|
|
89
|
-
self.
|
|
92
|
+
self.assertTrue(content.startswith("# Run 'tloop edit --help'"))
|
|
90
93
|
|
|
91
94
|
def test_idempotent_no_exit_when_tasks_exist(self):
|
|
92
95
|
self.home.mkdir(parents=True, exist_ok=True)
|
|
@@ -765,7 +768,7 @@ class RunTaskGitIntegrationTests(unittest.TestCase):
|
|
|
765
768
|
"prompt": "do something",
|
|
766
769
|
"branch": "feat/test",
|
|
767
770
|
}
|
|
768
|
-
result = task_mod.run_task(task, 0, state
|
|
771
|
+
result = task_mod.run_task(task, 0, state)
|
|
769
772
|
self.assertTrue(result)
|
|
770
773
|
mock_clean.assert_called_once()
|
|
771
774
|
mock_branch.assert_called_once_with(self.tmpdir, "feat/test")
|
|
@@ -778,7 +781,7 @@ class RunTaskGitIntegrationTests(unittest.TestCase):
|
|
|
778
781
|
"dir": self.tmpdir,
|
|
779
782
|
"prompt": "do something",
|
|
780
783
|
}
|
|
781
|
-
result = task_mod.run_task(task, 0, state
|
|
784
|
+
result = task_mod.run_task(task, 0, state)
|
|
782
785
|
self.assertFalse(result)
|
|
783
786
|
self.assertEqual(state["tasks"]["0"]["status"], "failed")
|
|
784
787
|
self.assertIn("auto-commit", state["tasks"]["0"]["error"])
|
|
@@ -793,7 +796,7 @@ class RunTaskGitIntegrationTests(unittest.TestCase):
|
|
|
793
796
|
"prompt": "do something",
|
|
794
797
|
"branch": True,
|
|
795
798
|
}
|
|
796
|
-
result = task_mod.run_task(task, 0, state
|
|
799
|
+
result = task_mod.run_task(task, 0, state)
|
|
797
800
|
self.assertFalse(result)
|
|
798
801
|
self.assertEqual(state["tasks"]["0"]["status"], "failed")
|
|
799
802
|
self.assertIn("branch", state["tasks"]["0"]["error"])
|
|
@@ -806,10 +809,63 @@ class RunTaskGitIntegrationTests(unittest.TestCase):
|
|
|
806
809
|
"dir": self.tmpdir,
|
|
807
810
|
"prompt": "do something",
|
|
808
811
|
}
|
|
809
|
-
task_mod.run_task(task, 0, state
|
|
812
|
+
task_mod.run_task(task, 0, state)
|
|
810
813
|
self.assertNotEqual(state["tasks"]["0"]["status"], "running")
|
|
811
814
|
self.assertEqual(state["tasks"]["0"]["status"], "failed")
|
|
812
815
|
|
|
813
816
|
|
|
817
|
+
class EditorSelectionTests(unittest.TestCase):
|
|
818
|
+
"""Tests for editor selection and persistence in cmd_edit."""
|
|
819
|
+
|
|
820
|
+
def setUp(self):
|
|
821
|
+
self.tmpdir = tempfile.mkdtemp()
|
|
822
|
+
self.home = Path(self.tmpdir) / "tloop"
|
|
823
|
+
self.home.mkdir(parents=True)
|
|
824
|
+
self.patches = [
|
|
825
|
+
patch.object(config, "TLOOP_HOME", self.home),
|
|
826
|
+
patch.object(config, "SETTINGS_FILE", self.home / "settings.json"),
|
|
827
|
+
]
|
|
828
|
+
for p in self.patches:
|
|
829
|
+
p.start()
|
|
830
|
+
|
|
831
|
+
def tearDown(self):
|
|
832
|
+
for p in self.patches:
|
|
833
|
+
p.stop()
|
|
834
|
+
|
|
835
|
+
def test_cli_editor_overrides_settings(self):
|
|
836
|
+
cmd_edit._save_settings({"editor": "vim"})
|
|
837
|
+
result = cmd_edit._resolve_editor("code")
|
|
838
|
+
self.assertEqual(result, "code")
|
|
839
|
+
|
|
840
|
+
def test_saved_editor_used_when_no_cli_override(self):
|
|
841
|
+
cmd_edit._save_settings({"editor": "code"})
|
|
842
|
+
result = cmd_edit._resolve_editor()
|
|
843
|
+
self.assertEqual(result, "code")
|
|
844
|
+
|
|
845
|
+
def test_prompt_on_first_run(self):
|
|
846
|
+
with patch("builtins.input", side_effect=["1"]):
|
|
847
|
+
with patch.object(cmd_edit, "KNOWN_EDITORS", {"code": ("VS Code", "code")}):
|
|
848
|
+
with patch("shutil.which", return_value="/usr/local/bin/code"):
|
|
849
|
+
result = cmd_edit._resolve_editor()
|
|
850
|
+
self.assertEqual(result, "code")
|
|
851
|
+
settings = json.loads((self.home / "settings.json").read_text())
|
|
852
|
+
self.assertEqual(settings["editor"], "code")
|
|
853
|
+
|
|
854
|
+
def test_custom_editor_via_prompt(self):
|
|
855
|
+
with patch("builtins.input", side_effect=["2", "subl"]):
|
|
856
|
+
with patch.object(cmd_edit, "KNOWN_EDITORS", {"code": ("VS Code", "code")}):
|
|
857
|
+
with patch("shutil.which", return_value="/usr/local/bin/code"):
|
|
858
|
+
result = cmd_edit._resolve_editor()
|
|
859
|
+
self.assertEqual(result, "subl")
|
|
860
|
+
|
|
861
|
+
def test_corrupt_settings_file_triggers_prompt(self):
|
|
862
|
+
(self.home / "settings.json").write_text("not json")
|
|
863
|
+
with patch("builtins.input", side_effect=["1"]):
|
|
864
|
+
with patch.object(cmd_edit, "KNOWN_EDITORS", {"code": ("VS Code", "code")}):
|
|
865
|
+
with patch("shutil.which", return_value="/usr/local/bin/code"):
|
|
866
|
+
result = cmd_edit._resolve_editor()
|
|
867
|
+
self.assertEqual(result, "code")
|
|
868
|
+
|
|
869
|
+
|
|
814
870
|
if __name__ == "__main__":
|
|
815
871
|
unittest.main()
|
tloop_cli-0.4.0/src/cmd_edit.py
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"""tloop edit — open ~/.tloop/tasks.yaml in editor."""
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import os
|
|
5
|
-
import subprocess
|
|
6
|
-
|
|
7
|
-
import config
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
EDIT_HELP = """\
|
|
11
|
-
Open ~/.tloop/tasks.yaml in your editor ($EDITOR, defaults to vi).
|
|
12
|
-
|
|
13
|
-
Task file format (~/.tloop/tasks.yaml):
|
|
14
|
-
|
|
15
|
-
defaults:
|
|
16
|
-
model: opus # optional default model
|
|
17
|
-
|
|
18
|
-
tasks:
|
|
19
|
-
- name: My task
|
|
20
|
-
dir: ~/projects/my-project
|
|
21
|
-
prompt: |
|
|
22
|
-
Describe what Claude should do.
|
|
23
|
-
# OR:
|
|
24
|
-
prompt_file: ./prompts/my-task.md
|
|
25
|
-
model: opus # optional override
|
|
26
|
-
branch: true # true=auto, "custom/name", false=skip
|
|
27
|
-
|
|
28
|
-
Each task runs in the specified directory. Completed tasks are
|
|
29
|
-
archived to ~/.tloop/archive/ after each run cycle.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def add_parser(subparsers):
|
|
34
|
-
p = subparsers.add_parser(
|
|
35
|
-
"edit",
|
|
36
|
-
help="Open ~/.tloop/tasks.yaml in editor",
|
|
37
|
-
description=EDIT_HELP,
|
|
38
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
39
|
-
)
|
|
40
|
-
p.set_defaults(func=handle)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def handle(args):
|
|
44
|
-
editor = os.environ.get("EDITOR", "vi")
|
|
45
|
-
subprocess.run([editor, str(config.TASKS_FILE)])
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"""Cybervisor runner backend."""
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
|
|
5
|
-
from runner import Runner
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class CybervisorRunner(Runner):
|
|
9
|
-
def run(self, prompt, cwd, model=None, log_file=None):
|
|
10
|
-
cmd = ["cybervisor", "run", prompt]
|
|
11
|
-
if model:
|
|
12
|
-
cmd.extend(["--model", model])
|
|
13
|
-
|
|
14
|
-
with open(log_file, "a") if log_file else open("/dev/null", "a") as log:
|
|
15
|
-
process = subprocess.Popen(
|
|
16
|
-
cmd,
|
|
17
|
-
cwd=cwd,
|
|
18
|
-
stdout=subprocess.PIPE,
|
|
19
|
-
stderr=subprocess.STDOUT,
|
|
20
|
-
text=True,
|
|
21
|
-
)
|
|
22
|
-
for line in process.stdout:
|
|
23
|
-
print(line, end="")
|
|
24
|
-
log.write(line)
|
|
25
|
-
log.flush()
|
|
26
|
-
process.wait()
|
|
27
|
-
|
|
28
|
-
return process.returncode
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|