copa-cli 0.2.0__tar.gz → 0.2.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.
- {copa_cli-0.2.0/copa_cli.egg-info → copa_cli-0.2.1}/PKG-INFO +2 -2
- {copa_cli-0.2.0 → copa_cli-0.2.1}/README.md +1 -1
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/cli.py +58 -0
- copa_cli-0.2.1/copa/copa.zsh +156 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1/copa_cli.egg-info}/PKG-INFO +2 -2
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa_cli.egg-info/SOURCES.txt +1 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/pyproject.toml +4 -1
- {copa_cli-0.2.0 → copa_cli-0.2.1}/LICENSE +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/__init__.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/__main__.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/cli_common.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/cli_internal.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/cli_llm.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/cli_share.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/config.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/db.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/evolve.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/fzf.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/history.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/llm.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/mcp_server.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/models.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/scanner.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/scoring.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa/sharing.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa_cli.egg-info/dependency_links.txt +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa_cli.egg-info/entry_points.txt +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa_cli.egg-info/requires.txt +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/copa_cli.egg-info/top_level.txt +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/setup.cfg +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/tests/test_cli_and_sharing.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/tests/test_db.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/tests/test_fzf.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/tests/test_models.py +0 -0
- {copa_cli-0.2.0 → copa_cli-0.2.1}/tests/test_scanner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: copa-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Command Palette — smart command tracking, ranking, and sharing for your shell
|
|
5
5
|
Author: Mark Stanford
|
|
6
6
|
License-Expression: MIT
|
|
@@ -87,7 +87,7 @@ pip install copa-cli[ollama]
|
|
|
87
87
|
Add this line to your `~/.zshrc`:
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
|
|
90
|
+
eval "$(copa init zsh)"
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
Then restart your shell or run `source ~/.zshrc`. This does three things:
|
|
@@ -20,6 +20,64 @@ def cli():
|
|
|
20
20
|
pass
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
# --- init ---
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@cli.command()
|
|
27
|
+
@click.argument("shell", type=click.Choice(["zsh"]))
|
|
28
|
+
def init(shell: str):
|
|
29
|
+
"""Print shell integration code. Add to your .zshrc: eval "$(copa init zsh)" """
|
|
30
|
+
from importlib.resources import files
|
|
31
|
+
|
|
32
|
+
zsh_file = files("copa").joinpath("copa.zsh")
|
|
33
|
+
click.echo(zsh_file.read_text())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# --- uninstall ---
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@cli.command()
|
|
40
|
+
@click.option("-y", "--yes", is_flag=True, help="Skip confirmation prompt.")
|
|
41
|
+
def uninstall(yes: bool):
|
|
42
|
+
"""Remove Copa data and show cleanup instructions."""
|
|
43
|
+
import shutil
|
|
44
|
+
|
|
45
|
+
copa_dir = Path.home() / ".copa"
|
|
46
|
+
|
|
47
|
+
# Inventory what exists
|
|
48
|
+
items: list[tuple[str, Path]] = []
|
|
49
|
+
if copa_dir.is_dir():
|
|
50
|
+
db_path = copa_dir / "copa.db"
|
|
51
|
+
config_path = copa_dir / "config.toml"
|
|
52
|
+
if db_path.is_file():
|
|
53
|
+
items.append(("database", db_path))
|
|
54
|
+
if config_path.is_file():
|
|
55
|
+
items.append(("config", config_path))
|
|
56
|
+
# Count any other files (.copa exports, etc.)
|
|
57
|
+
other = [f for f in copa_dir.iterdir() if f not in (db_path, config_path)]
|
|
58
|
+
for f in other:
|
|
59
|
+
items.append(("file", f))
|
|
60
|
+
|
|
61
|
+
if not items:
|
|
62
|
+
click.echo("No Copa data found (~/.copa/ does not exist or is empty).")
|
|
63
|
+
else:
|
|
64
|
+
click.echo("Copa data directory: ~/.copa/")
|
|
65
|
+
for kind, path in items:
|
|
66
|
+
size = path.stat().st_size if path.is_file() else 0
|
|
67
|
+
label = f"{size:,} bytes" if size else "directory"
|
|
68
|
+
click.echo(f" {path.name} ({kind}, {label})")
|
|
69
|
+
|
|
70
|
+
if not yes:
|
|
71
|
+
click.confirm("\nDelete ~/.copa/ and all its contents?", abort=True)
|
|
72
|
+
|
|
73
|
+
shutil.rmtree(copa_dir)
|
|
74
|
+
click.echo(click.style("Deleted ~/.copa/", fg="green"))
|
|
75
|
+
|
|
76
|
+
click.echo("\nTo finish uninstalling:")
|
|
77
|
+
click.echo(' 1. Remove this line from your ~/.zshrc: eval "$(copa init zsh)"')
|
|
78
|
+
click.echo(" 2. Run: pip uninstall copa-cli")
|
|
79
|
+
|
|
80
|
+
|
|
23
81
|
# --- add ---
|
|
24
82
|
|
|
25
83
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Copa — shell integration for zsh
|
|
2
|
+
# Add to your .zshrc: eval "$(copa init zsh)"
|
|
3
|
+
#
|
|
4
|
+
# What this does:
|
|
5
|
+
# 1. Records every command you run (precmd hook, background, zero latency)
|
|
6
|
+
# 2. Replaces Ctrl+R with an fzf command palette that searches across
|
|
7
|
+
# command text AND descriptions — not just raw history
|
|
8
|
+
# 3. Ctrl+R cycles modes inside fzf: all → frequent → recent → all
|
|
9
|
+
# 4. Composition keys append shell operators (|, &&, &, etc.) to selected commands
|
|
10
|
+
|
|
11
|
+
# --- Load keybinding config (runs once at shell startup) ---
|
|
12
|
+
eval "$(copa _fzf-config 2>/dev/null)" || {
|
|
13
|
+
# Fallback defaults if copa _fzf-config fails
|
|
14
|
+
_COPA_EXPECT='ctrl-v,ctrl-o,ctrl-x,ctrl-t,ctrl-a,ctrl-/'
|
|
15
|
+
_COPA_DESCRIBE_KEY='ctrl-d'
|
|
16
|
+
_COPA_GROUP_KEY='ctrl-g'
|
|
17
|
+
_COPA_FLAGS_KEY='ctrl-f'
|
|
18
|
+
_COPA_FILTER_GROUP_KEY='ctrl-s'
|
|
19
|
+
_COPA_CYCLE_GROUP_KEY='ctrl-n'
|
|
20
|
+
_COPA_COMPLETION_BRANDING='true'
|
|
21
|
+
_COPA_HEADER='Copa | ^R:cycle | ^V:& | ^O:2>&1 | ^X:| | ^T:> | ^A:&& | ^/:quiet | ^G:grp | ^D:desc | ^F:flag | ^S:scope | ^N:↻grp'
|
|
22
|
+
typeset -gA _COPA_SUFFIXES
|
|
23
|
+
_COPA_SUFFIXES[ctrl-v]=' &'
|
|
24
|
+
_COPA_SUFFIXES[ctrl-o]=' 2>&1'
|
|
25
|
+
_COPA_SUFFIXES[ctrl-x]=' | '
|
|
26
|
+
_COPA_SUFFIXES[ctrl-t]=' > '
|
|
27
|
+
_COPA_SUFFIXES[ctrl-a]=' && '
|
|
28
|
+
_COPA_SUFFIXES[ctrl-/]=' 2>/dev/null'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# --- precmd hook: record last command ---
|
|
32
|
+
_copa_precmd() {
|
|
33
|
+
local last_cmd
|
|
34
|
+
last_cmd="$(fc -ln -1 2>/dev/null)"
|
|
35
|
+
last_cmd="${last_cmd## }" # strip leading space
|
|
36
|
+
if [[ -n "$last_cmd" && "$last_cmd" != _copa_* ]]; then
|
|
37
|
+
copa _record "$last_cmd" &!
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Add to precmd hooks (avoid duplicates)
|
|
42
|
+
autoload -Uz add-zsh-hook
|
|
43
|
+
add-zsh-hook precmd _copa_precmd
|
|
44
|
+
|
|
45
|
+
# --- Ctrl+R: fzf-powered command palette ---
|
|
46
|
+
# Replaces default zsh reverse-search. fzf searches all visible fields:
|
|
47
|
+
# command text, description, group badges, shared-set names, and frequency.
|
|
48
|
+
# Selected command is placed into the prompt without executing.
|
|
49
|
+
# Composition keys (configured via copa _fzf-config) append shell operators.
|
|
50
|
+
# Requires fzf: brew install fzf
|
|
51
|
+
_copa_fzf_widget() {
|
|
52
|
+
if ! command -v fzf &>/dev/null; then
|
|
53
|
+
zle -M "Copa: fzf not found. Install with: brew install fzf"
|
|
54
|
+
return 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
local mode="all"
|
|
58
|
+
local output
|
|
59
|
+
local copa_bin="${commands[copa]:-copa}"
|
|
60
|
+
|
|
61
|
+
output=$("$copa_bin" fzf-list --mode "$mode" | \
|
|
62
|
+
fzf --ansi \
|
|
63
|
+
--delimiter '┃' \
|
|
64
|
+
--with-nth '2..3' \
|
|
65
|
+
--preview "$copa_bin _preview {1}" \
|
|
66
|
+
--preview-window 'right:40%:wrap' \
|
|
67
|
+
--header "$_COPA_HEADER" \
|
|
68
|
+
--prompt 'copa> ' \
|
|
69
|
+
--height '80%' \
|
|
70
|
+
--layout reverse \
|
|
71
|
+
--expect "$_COPA_EXPECT" \
|
|
72
|
+
--bind "${_COPA_DESCRIBE_KEY}:execute($copa_bin describe {1})+refresh-preview" \
|
|
73
|
+
--bind "${_COPA_GROUP_KEY}:execute($copa_bin _set-group {1})+reload($copa_bin fzf-list)+refresh-preview" \
|
|
74
|
+
--bind "${_COPA_FLAGS_KEY}:execute($copa_bin _set-flags {1})+reload($copa_bin fzf-list)+refresh-preview" \
|
|
75
|
+
--bind "${_COPA_FILTER_GROUP_KEY}:transform:
|
|
76
|
+
group=\$(${copa_bin} _list-groups | fzf --height 40% --layout reverse --prompt 'group> ' --header 'Select group (ESC=cancel)');
|
|
77
|
+
if [[ \$group == '(all)' ]]; then
|
|
78
|
+
echo \"reload(${copa_bin} fzf-list --mode all)+change-prompt(copa> )\"
|
|
79
|
+
elif [[ -n \$group ]]; then
|
|
80
|
+
echo \"reload(${copa_bin} fzf-list --mode group --group \$group)+change-prompt(copa [\$group]> )\"
|
|
81
|
+
fi" \
|
|
82
|
+
--bind "${_COPA_CYCLE_GROUP_KEY}:transform:
|
|
83
|
+
cur_group='(all)';
|
|
84
|
+
if [[ \$FZF_PROMPT =~ 'copa \\[(.+)\\]> ' ]]; then
|
|
85
|
+
cur_group=\"\${match[1]}\";
|
|
86
|
+
fi;
|
|
87
|
+
next=\$(${copa_bin} _next-group \"\$cur_group\");
|
|
88
|
+
if [[ \$next == '(all)' ]]; then
|
|
89
|
+
echo \"reload(${copa_bin} fzf-list --mode all)+change-prompt(copa> )\";
|
|
90
|
+
else
|
|
91
|
+
echo \"reload(${copa_bin} fzf-list --mode group --group \$next)+change-prompt(copa [\$next]> )\";
|
|
92
|
+
fi" \
|
|
93
|
+
--bind 'ctrl-r:transform:
|
|
94
|
+
if [[ $FZF_PROMPT == "copa> " ]]; then
|
|
95
|
+
echo "reload('"$copa_bin"' fzf-list --mode frequent)+change-prompt(frequent> )"
|
|
96
|
+
elif [[ $FZF_PROMPT == "frequent> " ]]; then
|
|
97
|
+
echo "reload('"$copa_bin"' fzf-list --mode recent)+change-prompt(recent> )"
|
|
98
|
+
else
|
|
99
|
+
echo "reload('"$copa_bin"' fzf-list --mode all)+change-prompt(copa> )"
|
|
100
|
+
fi' \
|
|
101
|
+
--bind 'enter:accept' \
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if [[ -n "$output" ]]; then
|
|
105
|
+
# --expect output: line 1 = key pressed (empty for Enter), line 2+ = selected item
|
|
106
|
+
local key selected cmd suffix
|
|
107
|
+
key=$(echo "$output" | head -1)
|
|
108
|
+
selected=$(echo "$output" | tail -n +2)
|
|
109
|
+
|
|
110
|
+
if [[ -n "$selected" ]]; then
|
|
111
|
+
cmd=$(echo "$selected" | cut -d'┃' -f2 | sed 's/^ *//;s/ *$//')
|
|
112
|
+
suffix="${_COPA_SUFFIXES[$key]}"
|
|
113
|
+
LBUFFER="${cmd}${suffix}"
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
zle reset-prompt
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
zle -N _copa_fzf_widget
|
|
121
|
+
bindkey '^R' _copa_fzf_widget
|
|
122
|
+
|
|
123
|
+
# --- Tab completion ---
|
|
124
|
+
# Ensure zsh completion system is available, then register Copa completions.
|
|
125
|
+
if ! (( $+functions[compdef] )); then
|
|
126
|
+
autoload -Uz compinit && compinit -i -C
|
|
127
|
+
fi
|
|
128
|
+
eval "$(copa completion zsh)"
|
|
129
|
+
|
|
130
|
+
# --- Supplemental tab completion from Copa database ---
|
|
131
|
+
# Registers as a fallback completer so that any command (e.g. adb <TAB>)
|
|
132
|
+
# gets completion candidates from Copa's command history.
|
|
133
|
+
_copa_history_complete() {
|
|
134
|
+
# Only provide Copa completions when primary completers found nothing
|
|
135
|
+
(( compstate[nmatches] > 0 )) && return
|
|
136
|
+
# Skip empty tokens (bare <TAB> with no partial input)
|
|
137
|
+
[[ -z "${words[CURRENT]}" ]] && return
|
|
138
|
+
# Skip internal copa commands
|
|
139
|
+
[[ "${words[CURRENT]}" == _copa_* ]] && return
|
|
140
|
+
local -a results
|
|
141
|
+
results=("${(@f)$(copa _complete-word "${(@)words[1,CURRENT]}" 2>/dev/null)}")
|
|
142
|
+
(( ${#results} )) && compadd -V 'copa-history' -o nosort -- "${results[@]}"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Append to existing completers without clobbering user config
|
|
146
|
+
() {
|
|
147
|
+
local -a cur
|
|
148
|
+
zstyle -g cur ':completion:*' completer 2>/dev/null
|
|
149
|
+
if (( ! ${cur[(Ie)_copa_history_complete]} )); then
|
|
150
|
+
zstyle ':completion:*' completer ${cur:-_complete} _copa_history_complete
|
|
151
|
+
fi
|
|
152
|
+
# Copa completion branding: show "Copa history" header when branding is enabled
|
|
153
|
+
if [[ "$_COPA_COMPLETION_BRANDING" != 'false' ]]; then
|
|
154
|
+
zstyle ':completion:*:*:*:copa-history' group-header '%F{magenta}-- Copa history --%f'
|
|
155
|
+
fi
|
|
156
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: copa-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Command Palette — smart command tracking, ranking, and sharing for your shell
|
|
5
5
|
Author: Mark Stanford
|
|
6
6
|
License-Expression: MIT
|
|
@@ -87,7 +87,7 @@ pip install copa-cli[ollama]
|
|
|
87
87
|
Add this line to your `~/.zshrc`:
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
|
|
90
|
+
eval "$(copa init zsh)"
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
Then restart your shell or run `source ~/.zshrc`. This does three things:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "copa-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Command Palette — smart command tracking, ranking, and sharing for your shell"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -42,6 +42,9 @@ copa = "copa.cli:cli"
|
|
|
42
42
|
[tool.setuptools.packages.find]
|
|
43
43
|
include = ["copa*"]
|
|
44
44
|
|
|
45
|
+
[tool.setuptools.package-data]
|
|
46
|
+
copa = ["copa.zsh"]
|
|
47
|
+
|
|
45
48
|
[tool.ruff]
|
|
46
49
|
target-version = "py312"
|
|
47
50
|
line-length = 120
|
|
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
|
|
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
|