imp-git 0.0.26__py3-none-any.whl

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.
imp/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ from importlib.metadata import PackageNotFoundError
2
+ from importlib.metadata import version as _v
3
+
4
+ try:
5
+ __version__ = _v ("imp")
6
+ except PackageNotFoundError:
7
+ __version__ = "dev"
imp/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from imp.main import app
2
+
3
+ app ()
imp/ai.py ADDED
@@ -0,0 +1,130 @@
1
+ import json
2
+ import subprocess
3
+ import urllib.error
4
+ import urllib.request
5
+
6
+ import typer
7
+
8
+ from imp import config, console
9
+
10
+ MAX_DIFF_LINES = 2000
11
+
12
+
13
+ def _claude (prompt: str, model: str) -> str:
14
+ result = subprocess.run (
15
+ [
16
+ "claude", "-p",
17
+ "--model", model,
18
+ "--max-turns", "1",
19
+ "--tools", "",
20
+ ],
21
+ input=prompt,
22
+ capture_output=True,
23
+ text=True,
24
+ )
25
+
26
+ if result.returncode != 0:
27
+ console.err ("claude CLI failed")
28
+ raise typer.Exit (1)
29
+
30
+ return result.stdout
31
+
32
+
33
+ def _ollama (prompt: str, model: str) -> str:
34
+ payload = json.dumps ({
35
+ "model": model,
36
+ "prompt": prompt,
37
+ "stream": False,
38
+ }).encode ()
39
+
40
+ req = urllib.request.Request (
41
+ "http://localhost:11434/api/generate",
42
+ data=payload,
43
+ headers={"Content-Type": "application/json"},
44
+ )
45
+
46
+ try:
47
+ with urllib.request.urlopen (req) as resp:
48
+ body = json.loads (resp.read ())
49
+ return body.get ("response", "")
50
+ except (urllib.error.URLError, json.JSONDecodeError, OSError) as e:
51
+ console.err (f"ollama request failed: {e}")
52
+ raise typer.Exit (1) from None
53
+
54
+
55
+ def _call (prompt: str, model: str) -> str:
56
+ provider = config.get ("provider")
57
+ if provider == "claude":
58
+ return _claude (prompt, model)
59
+ elif provider == "ollama":
60
+ return _ollama (prompt, model)
61
+ else:
62
+ console.err (f"Unknown AI provider: {provider}")
63
+ raise typer.Exit (1)
64
+
65
+
66
+ def fast (prompt: str, spin: bool = True) -> str:
67
+ model = config.get ("model:fast")
68
+ if spin:
69
+ result = console.spin ("Thinking...", _call, prompt, model)
70
+ else:
71
+ result = _call (prompt, model)
72
+ if not result or not result.strip ():
73
+ console.err ("Empty response from AI")
74
+ raise typer.Exit (1)
75
+
76
+ return result
77
+
78
+
79
+ def smart (prompt: str, spin: bool = True) -> str:
80
+ model = config.get ("model:smart")
81
+ if spin:
82
+ result = console.spin ("Thinking...", _call, prompt, model)
83
+ else:
84
+ result = _call (prompt, model)
85
+ if not result or not result.strip ():
86
+ console.err ("Empty response from AI")
87
+ raise typer.Exit (1)
88
+
89
+ return result
90
+
91
+
92
+ def ping () -> bool:
93
+ try:
94
+ model = config.get ("model:fast")
95
+ result = _call ("Reply with OK", model)
96
+ return bool (result and result.strip ())
97
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
98
+ return False
99
+ except SystemExit:
100
+ return False
101
+
102
+
103
+ def oneline (text: str) -> str:
104
+ return text.replace ("\n", "").strip ()
105
+
106
+
107
+ def truncate (text: str, max_lines: int = MAX_DIFF_LINES) -> str:
108
+ lines = text.splitlines ()
109
+ if len (lines) <= max_lines:
110
+ return text
111
+ return "\n".join (lines [:max_lines])
112
+
113
+
114
+ def commit_message (prompt: str) -> str:
115
+ from imp import validate
116
+
117
+ msg = fast (prompt)
118
+ msg = oneline (msg)
119
+
120
+ if not validate.commit (msg):
121
+ console.warn ("Retrying (invalid format)...")
122
+ msg = fast (prompt)
123
+ msg = oneline (msg)
124
+
125
+ if not validate.commit (msg):
126
+ console.err ("AI output not in Conventional Commits format")
127
+ console.muted (msg)
128
+ raise typer.Exit (1)
129
+
130
+ return msg
File without changes
imp/commands/amend.py ADDED
@@ -0,0 +1,68 @@
1
+ import typer
2
+
3
+ from imp import ai, console, git, prompts
4
+
5
+
6
+ def amend (
7
+ yes: bool = typer.Option (False, "--yes", "-y", help="Accept AI message without review"),
8
+ whisper: str = typer.Option ("", "--whisper", "-w", help="Hint to guide the AI"),
9
+ ):
10
+ """Amend last commit with a new AI-generated message.
11
+
12
+ Stages any uncommitted changes, regenerates the commit message from the
13
+ full diff, and amends the previous commit. You can review, edit, or
14
+ cancel the new message before it's applied.
15
+ """
16
+
17
+ git.require ()
18
+
19
+ console.header ("Amend")
20
+
21
+ total = git.commit_count ()
22
+ if total == 0:
23
+ console.err ("No commits to amend")
24
+ console.hint ("imp commit first")
25
+ raise typer.Exit (1)
26
+
27
+ last_msg = git.show ("HEAD", fmt="%s")
28
+
29
+ if total == 1:
30
+ combined = git.show ("HEAD")
31
+ changes = git.diff ()
32
+ if changes:
33
+ combined = combined + "\n" + changes
34
+ else:
35
+ combined = git.diff_range ("HEAD~1")
36
+
37
+ if not combined:
38
+ console.err ("Nothing to amend")
39
+ raise typer.Exit (1)
40
+
41
+ msg = ai.commit_message (prompts.commit (combined, whisper=whisper))
42
+
43
+ console.label ("Previous")
44
+ console.item (last_msg)
45
+ console.out.print ()
46
+
47
+ git.stage (all=True)
48
+
49
+ if yes:
50
+ console.item (msg)
51
+ git.commit (msg, amend=True)
52
+ else:
53
+ choice = console.review (msg)
54
+
55
+ if choice == "Edit":
56
+ msg = console.edit (msg)
57
+ if not msg.strip ():
58
+ console.muted ("Empty message, cancelled")
59
+ raise typer.Exit (0)
60
+ git.commit (msg, amend=True)
61
+ elif choice == "Yes":
62
+ git.commit (msg, amend=True)
63
+ else:
64
+ console.muted ("Cancelled")
65
+ raise typer.Exit (0)
66
+
67
+ console.success ("Amended")
68
+ console.hint ("imp commit again, or imp release when ready")
imp/commands/branch.py ADDED
@@ -0,0 +1,72 @@
1
+ import typer
2
+
3
+ from imp import ai, console, git, prompts, validate
4
+
5
+
6
+ def branch (
7
+ description: list [str] | None = typer.Argument (None, help="Branch description"),
8
+ yes: bool = typer.Option (False, "--yes", "-y", help="Accept suggested branch name"),
9
+ whisper: str = typer.Option ("", "--whisper", "-w", help="Hint to guide the AI"),
10
+ ):
11
+ """Create or switch branches. No args: interactive picker.
12
+
13
+ With arguments, uses AI to generate a clean branch name from your
14
+ description. Without arguments, shows local branches with their age
15
+ and lets you pick one to switch to.
16
+ """
17
+
18
+ git.require ()
19
+
20
+ if not description:
21
+ _switch ()
22
+ else:
23
+ _create (" ".join (description), whisper, yes)
24
+
25
+
26
+ def _switch ():
27
+ console.header ("Branches")
28
+
29
+ branches = git.branches_local ()
30
+ if len (branches) <= 1:
31
+ console.muted ("Only one branch exists")
32
+ raise typer.Exit (0)
33
+
34
+ labels = []
35
+ for b in branches:
36
+ age = git.branch_age (b)
37
+ labels.append (f"{b} ({age})")
38
+
39
+ choice = console.choose ("Switch to:", labels)
40
+ target = choice.split (" (") [0]
41
+
42
+ current = git.branch ()
43
+ if target == current:
44
+ console.muted (f"Already on {target}")
45
+ raise typer.Exit (0)
46
+
47
+ git.require_clean ()
48
+ git.checkout (target)
49
+ console.success (f"Switched to {target}")
50
+
51
+
52
+ def _create (desc: str, whisper: str = "", yes: bool = False):
53
+ console.header ("Branch")
54
+
55
+ name = ai.fast (prompts.branch_name (desc, whisper))
56
+ name = ai.oneline (name)
57
+
58
+ if not validate.branch (name):
59
+ console.err (f"Invalid branch name: {name}")
60
+ raise typer.Exit (1)
61
+
62
+ console.label ("Suggested")
63
+ console.item (name)
64
+ console.out.print ()
65
+
66
+ if yes or console.confirm ("Create branch?"):
67
+ git.checkout (name, create=True)
68
+ console.success (f"Switched to {name}")
69
+ console.hint ("make changes, then imp commit")
70
+ else:
71
+ console.muted ("Cancelled")
72
+ raise typer.Exit (0)
imp/commands/clean.py ADDED
@@ -0,0 +1,47 @@
1
+ import typer
2
+
3
+ from imp import console, git
4
+
5
+
6
+ def clean ():
7
+ """Delete branches already merged into base.
8
+
9
+ Fetches from origin, finds local branches fully merged into the base
10
+ branch (main/master), and deletes them locally and remotely. Prompts
11
+ for confirmation before deleting.
12
+ """
13
+
14
+ git.require ()
15
+
16
+ console.header ("Clean")
17
+
18
+ base = git.base_branch ()
19
+
20
+ console.spin ("Fetching...", git.fetch, True)
21
+
22
+ merged = git.branches_merged (base)
23
+
24
+ if not merged:
25
+ console.success ("No merged branches to clean")
26
+ raise typer.Exit (0)
27
+
28
+ console.label (f"Merged into {base}")
29
+ for b in merged:
30
+ console.item (b)
31
+ console.out.print ()
32
+
33
+ count = len (merged)
34
+ if not console.confirm (f"Delete {count} branch(es)?"):
35
+ console.muted ("Cancelled")
36
+ raise typer.Exit (0)
37
+
38
+ console.out.print ()
39
+ for b in merged:
40
+ git.delete_branch (b)
41
+ console.success (f"Deleted {b}")
42
+
43
+ if git.remote_has_branch (b):
44
+ git.delete_branch (b, remote=True)
45
+ console.success (f"Deleted remote {b}")
46
+
47
+ console.hint ("imp branch to start something new")
imp/commands/commit.py ADDED
@@ -0,0 +1,67 @@
1
+ from fnmatch import fnmatch
2
+ from typing import Optional
3
+
4
+ import typer
5
+
6
+ from imp import ai, console, git, prompts
7
+
8
+
9
+ def commit (
10
+ all: bool = typer.Option (False, "--all", "-a", help="Stage all changes first"),
11
+ exclude: Optional [list [str]] = typer.Option (None, "--exclude", "-E", help="Glob patterns to exclude from staging"),
12
+ yes: bool = typer.Option (False, "--yes", "-y", help="Accept AI message without review"),
13
+ whisper: str = typer.Option ("", "--whisper", "-w", help="Hint to guide the AI"),
14
+ ):
15
+ """Generate an AI commit message for staged changes.
16
+
17
+ Reads the staged diff and uses AI to produce a Conventional Commits
18
+ message. Use --all/-a to stage everything first. You can review, edit,
19
+ or cancel the message before committing.
20
+ """
21
+
22
+ git.require ()
23
+
24
+ if all:
25
+ git.stage (all=True)
26
+
27
+ if exclude:
28
+ staged = git.staged_files ()
29
+ to_unstage = [
30
+ f for f in staged
31
+ if any (fnmatch (f, pat) for pat in exclude)
32
+ ]
33
+ if to_unstage:
34
+ git.unstage (to_unstage)
35
+ console.muted (f"Excluded {len (to_unstage)} files")
36
+
37
+ d = git.diff (staged=True)
38
+ if not d:
39
+ console.err ("Nothing staged")
40
+ console.hint ("git add <files>, or imp commit -a")
41
+ raise typer.Exit (1)
42
+
43
+ console.header ("Commit")
44
+
45
+ b = git.branch ()
46
+ msg = ai.commit_message (prompts.commit (d, b, whisper))
47
+
48
+ if yes:
49
+ console.item (msg)
50
+ git.commit (msg)
51
+ else:
52
+ choice = console.review (msg)
53
+
54
+ if choice == "Edit":
55
+ msg = console.edit (msg)
56
+ if not msg.strip ():
57
+ console.muted ("Empty message, cancelled")
58
+ raise typer.Exit (0)
59
+ git.commit (msg)
60
+ elif choice == "Yes":
61
+ git.commit (msg)
62
+ else:
63
+ console.muted ("Cancelled")
64
+ raise typer.Exit (0)
65
+
66
+ console.success ("Committed")
67
+ console.hint ("imp commit again, or imp release when ready")
imp/commands/config.py ADDED
@@ -0,0 +1,62 @@
1
+ from imp import config, console
2
+
3
+
4
+ def _provider_choices () -> list [str]:
5
+ return [ "claude", "ollama" ]
6
+
7
+
8
+ def _claude_models () -> list [str]:
9
+ return [ "haiku", "sonnet", "opus" ]
10
+
11
+
12
+ def _ollama_models () -> list [str]:
13
+ return [
14
+ "llama3.2",
15
+ "llama3.1",
16
+ "mistral",
17
+ "codellama",
18
+ "custom",
19
+ ]
20
+
21
+
22
+ def configure ():
23
+ """Interactively configure AI provider and models.
24
+
25
+ Sets the AI provider (Claude or Ollama) and which models to use for
26
+ fast and smart operations. Configuration is stored in
27
+ ~/.config/imp/config.json.
28
+ """
29
+
30
+ console.header ("Config")
31
+
32
+ cfg = config.load ()
33
+
34
+ console.muted (f"Config: {config.path ()}")
35
+ console.out.print ()
36
+
37
+ provider = console.choose ("AI provider", _provider_choices ())
38
+ cfg ["provider"] = provider
39
+
40
+ if provider == "claude":
41
+ models = _claude_models ()
42
+ else:
43
+ models = _ollama_models ()
44
+
45
+ fast = console.choose ("Fast model (commits, branches)", models)
46
+ if fast == "custom":
47
+ fast = console.prompt ("Model name:")
48
+ cfg ["model:fast"] = fast
49
+
50
+ smart = console.choose ("Smart model (review, PR, split)", models)
51
+ if smart == "custom":
52
+ smart = console.prompt ("Model name:")
53
+ cfg ["model:smart"] = smart
54
+
55
+ config.save (cfg)
56
+
57
+ console.out.print ()
58
+ console.success ("Saved")
59
+ console.muted (f" provider: {cfg ['provider']}")
60
+ console.muted (f" model:fast: {cfg ['model:fast']}")
61
+ console.muted (f" model:smart: {cfg ['model:smart']}")
62
+ console.hint ("imp doctor to verify")
imp/commands/doctor.py ADDED
@@ -0,0 +1,87 @@
1
+ import shutil
2
+ import subprocess
3
+
4
+ import typer
5
+
6
+ from imp import ai, config, console
7
+
8
+
9
+ def _check (name: str, cmd: str, url: str, required: bool = True) -> bool:
10
+ path = shutil.which (cmd)
11
+ if path:
12
+ try:
13
+ result = subprocess.run (
14
+ [ cmd, "--version" ],
15
+ capture_output=True,
16
+ text=True,
17
+ timeout=5,
18
+ )
19
+ version = result.stdout.strip ().splitlines () [0] if result.stdout.strip () else "installed"
20
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
21
+ version = "installed"
22
+ console.success (f"{name} ({version})")
23
+ return True
24
+
25
+ if required:
26
+ console.err (f"{name} not found")
27
+ console.item (url)
28
+ return False
29
+
30
+ console.muted (f" {name} not found (optional)")
31
+ console.item (url)
32
+ return True
33
+
34
+
35
+ def doctor ():
36
+ """Check tools and configuration.
37
+
38
+ Verifies that required (git) and optional (claude, ollama, gh) tools are
39
+ installed, shows their versions, and confirms at least one AI provider is
40
+ available. Also displays the active provider and model settings from
41
+ ~/.config/imp/config.json.
42
+ """
43
+
44
+ console.header ("Doctor")
45
+
46
+ ok = True
47
+ ok = _check ("git", "git", "https://git-scm.com") and ok
48
+ _check ("claude", "claude", "https://docs.anthropic.com/en/docs/claude-code", required=False)
49
+ _check ("ollama", "ollama", "https://ollama.com", required=False)
50
+ _check ("gh", "gh", "https://cli.github.com", required=False)
51
+
52
+ console.out.print ()
53
+
54
+ has_claude = shutil.which ("claude") is not None
55
+ has_ollama = shutil.which ("ollama") is not None
56
+
57
+ if not has_claude and not has_ollama:
58
+ console.err ("No AI provider found (need claude or ollama)")
59
+ ok = False
60
+
61
+ cfg = config.load ()
62
+ provider = cfg ["provider"]
63
+ console.muted (f"Provider: {provider}")
64
+ console.muted (f"Fast model: {cfg ['model:fast']}")
65
+ console.muted (f"Smart model: {cfg ['model:smart']}")
66
+ console.muted (f"Config: {config.path ()}")
67
+
68
+ console.out.print ()
69
+
70
+ if has_claude or has_ollama:
71
+ if console.spin ("Testing AI connection...", ai.ping):
72
+ console.success ("AI responding")
73
+ else:
74
+ console.err ("AI not responding")
75
+ if provider == "claude":
76
+ console.hint ("run: claude to authenticate")
77
+ else:
78
+ console.hint ("is ollama running? try: ollama serve")
79
+ ok = False
80
+
81
+ console.out.print ()
82
+
83
+ if ok:
84
+ console.success ("All good")
85
+ else:
86
+ console.warn ("Some issues found")
87
+ raise typer.Exit (1)
imp/commands/done.py ADDED
@@ -0,0 +1,98 @@
1
+ import typer
2
+
3
+ from imp import console, git
4
+
5
+
6
+ def done (
7
+ target: str | None = typer.Argument (None, help="Branch to merge into"),
8
+ yes: bool = typer.Option (False, "--yes", "-y", help="Skip confirmation"),
9
+ ):
10
+ """Merge feature branch into target, then clean up.
11
+
12
+ Merges the current branch into a target branch (defaults to main/master),
13
+ then deletes the feature branch locally and remotely. If the branch was
14
+ already merged via PR, pulls the target instead of merging locally.
15
+ """
16
+
17
+ git.require ()
18
+ git.require_clean ("imp commit first")
19
+
20
+ feature = git.branch ()
21
+ base = target or git.base_branch ()
22
+
23
+ if feature == base:
24
+ console.err (f"Already on {base}")
25
+ console.hint ("switch to a feature branch first")
26
+ raise typer.Exit (1)
27
+
28
+ if target and not git.rev_parse (target):
29
+ if git.remote_has_branch (target):
30
+ git.checkout (target)
31
+ git.checkout (feature)
32
+ else:
33
+ console.err (f"Branch {target} does not exist")
34
+ raise typer.Exit (1)
35
+
36
+ console.header ("Done")
37
+
38
+ git.fetch (prune=True)
39
+
40
+ already_merged = False
41
+ remote_base = f"origin/{base}"
42
+ if git.rev_parse (remote_base):
43
+ already_merged = git.is_merged (feature, remote_base)
44
+
45
+ has_remote_feature = git.remote_has_branch (feature)
46
+
47
+ console.label (f"{feature} → {base}")
48
+ console.out.print ()
49
+ console.item (f"Switch to {base}")
50
+
51
+ if already_merged:
52
+ console.item ("Pull latest (already merged remotely)")
53
+ else:
54
+ console.item (f"Merge {feature} into {base} (--no-ff)")
55
+
56
+ console.item (f"Delete {feature} (local)")
57
+
58
+ if has_remote_feature:
59
+ console.item (f"Delete {feature} (remote)")
60
+
61
+ console.out.print ()
62
+
63
+ if not yes and not console.confirm ("Proceed?"):
64
+ console.muted ("Cancelled")
65
+ raise typer.Exit (0)
66
+
67
+ git.checkout (base)
68
+
69
+ if already_merged:
70
+ git.pull ()
71
+ console.success (f"Pulled {base} (already merged)")
72
+ else:
73
+ if git.has_upstream ():
74
+ git.pull ()
75
+
76
+ if not git.merge (feature, no_ff=True):
77
+ console.err ("Merge conflict")
78
+ console.hint ("imp resolve to fix conflicts")
79
+ raise typer.Exit (1)
80
+
81
+ console.success (f"Merged {feature} into {base}")
82
+
83
+ if git.delete_branch (feature):
84
+ console.success (f"Deleted local branch {feature}")
85
+ else:
86
+ console.warn (f"Branch {feature} has unmerged changes")
87
+ console.out.print ()
88
+ if console.confirm (f"Force delete {feature}?"):
89
+ git.delete_branch (feature, force=True)
90
+ console.success (f"Force deleted local branch {feature}")
91
+ else:
92
+ console.muted (f"Kept local branch {feature}")
93
+
94
+ if has_remote_feature:
95
+ git.delete_branch (feature, remote=True)
96
+ console.success (f"Deleted remote branch {feature}")
97
+
98
+ console.hint ("imp branch to start something new")