copa-cli 0.4.0__tar.gz → 0.6.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.
- {copa_cli-0.4.0 → copa_cli-0.6.0}/PKG-INFO +133 -7
- {copa_cli-0.4.0 → copa_cli-0.6.0}/README.md +132 -6
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/cli.py +196 -4
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/cli_internal.py +165 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/cli_llm.py +33 -12
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/config.py +72 -6
- copa_cli-0.6.0/copa/copa.zsh +578 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/db.py +3 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa_cli.egg-info/PKG-INFO +133 -7
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa_cli.egg-info/SOURCES.txt +1 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/pyproject.toml +1 -1
- copa_cli-0.6.0/tests/test_modal.py +1045 -0
- copa_cli-0.6.0/tests/test_polish.py +211 -0
- copa_cli-0.4.0/copa/copa.zsh +0 -197
- copa_cli-0.4.0/tests/test_modal.py +0 -216
- {copa_cli-0.4.0 → copa_cli-0.6.0}/LICENSE +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/__init__.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/__main__.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/cli_common.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/cli_share.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/evolve.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/fzf.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/history.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/llm.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/mcp_server.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/models.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/scanner.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/scoring.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa/sharing.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa_cli.egg-info/dependency_links.txt +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa_cli.egg-info/entry_points.txt +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa_cli.egg-info/requires.txt +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/copa_cli.egg-info/top_level.txt +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/setup.cfg +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/tests/test_cli_and_sharing.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/tests/test_db.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/tests/test_fzf.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/tests/test_models.py +0 -0
- {copa_cli-0.4.0 → copa_cli-0.6.0}/tests/test_scanner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: copa-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Command Palette — smart command tracking, ranking, and sharing for your shell
|
|
5
5
|
Author: Mark Stanford
|
|
6
6
|
License-Expression: MIT
|
|
@@ -52,11 +52,14 @@ Copa tracks the commands you run, ranks them by frequency and recency, and gives
|
|
|
52
52
|
- **MCP server** — expose your commands to Claude Code (or any MCP client)
|
|
53
53
|
- **Zero latency** — precmd hook records usage in the background
|
|
54
54
|
|
|
55
|
+
> **Note:** Copa requires **zsh**. It is not compatible with bash, fish, or PowerShell.
|
|
56
|
+
|
|
55
57
|
## Install
|
|
56
58
|
|
|
57
59
|
### Prerequisites
|
|
58
60
|
|
|
59
61
|
- **Python 3.12+**
|
|
62
|
+
- **zsh** — Copa's shell integration (precmd hooks, ZLE widgets, inline suggestions) is zsh-only
|
|
60
63
|
- **fzf** — required for Ctrl+R command palette
|
|
61
64
|
|
|
62
65
|
```bash
|
|
@@ -210,9 +213,62 @@ This works automatically once `copa.zsh` is sourced — no extra setup needed. T
|
|
|
210
213
|
|
|
211
214
|
Copa's own CLI completions (`copa li<TAB>` → `list`) continue to work as before via Click's built-in completion.
|
|
212
215
|
|
|
216
|
+
## Inline Suggestions (Ghost Text)
|
|
217
|
+
|
|
218
|
+
Copa shows grey ghost text after your cursor as you type — the best matching command from your database, ranked by frequency and recency. This works like fish shell's autosuggestions or zsh-autosuggestions, with zero plugin dependencies.
|
|
219
|
+
|
|
220
|
+
### How it works
|
|
221
|
+
|
|
222
|
+
As you type, Copa queries its database for commands starting with your current input and displays the highest-scored match as dim grey text after the cursor. The suggestion updates on every keystroke.
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
$ git pu█sh origin main ← grey ghost text
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Keybindings
|
|
229
|
+
|
|
230
|
+
| Key | Suggestion showing | No suggestion |
|
|
231
|
+
|-----|-------------------|---------------|
|
|
232
|
+
| Type chars | Insert char, re-fetch suggestion | Insert char, fetch suggestion |
|
|
233
|
+
| Backspace | Delete char, **latch** (suppress suggestions) | Delete char normally |
|
|
234
|
+
| **Tab** | `tab_accept=1`: accept full suggestion. `tab_accept=2` (default): first Tab highlights suggestion (cyan), second Tab accepts | If latched: unlatch + re-fetch suggestion. Else: normal tab completion |
|
|
235
|
+
| **Down** | Highlight suggestion (enter confirming state) | History navigation |
|
|
236
|
+
| **Right arrow** | Accept one word, re-fetch | Move cursor right |
|
|
237
|
+
| Enter | Clear suggestion, execute | Execute |
|
|
238
|
+
| Esc | Dismiss suggestion | Normal Esc |
|
|
239
|
+
| Up | Clear suggestion, navigate history | History navigation |
|
|
240
|
+
| Ctrl+R | Clear suggestion, open fzf | Open fzf |
|
|
241
|
+
|
|
242
|
+
### Tab accept mode
|
|
243
|
+
|
|
244
|
+
By default (`tab_accept = 2`), pressing Tab when a suggestion is showing highlights the ghost text (changes from dim grey to cyan/bold) to indicate it's ready to be accepted. Pressing Tab again accepts it into the buffer. Pressing Esc reverts to dim grey without accepting. This two-step flow gives you a visual confirmation before committing.
|
|
245
|
+
|
|
246
|
+
Set `tab_accept = 1` to restore the old behavior where a single Tab directly accepts the suggestion.
|
|
247
|
+
|
|
248
|
+
### Backspace latch
|
|
249
|
+
|
|
250
|
+
Pressing Backspace clears the current suggestion and **latches** — suppresses further suggestions while you edit. This prevents suggestions from re-appearing as you retype after correcting a mistake. Ctrl+W (backward-kill-word) also latches.
|
|
251
|
+
|
|
252
|
+
Press **Tab** to unlatch and re-enable suggestions. The next new prompt (Enter) also resets the latch automatically.
|
|
253
|
+
|
|
254
|
+
### Configuration
|
|
255
|
+
|
|
256
|
+
```toml
|
|
257
|
+
# ~/.copa/config.toml
|
|
258
|
+
[suggest]
|
|
259
|
+
enabled = true # set to false to disable inline suggestions
|
|
260
|
+
min_length = 2 # minimum characters before querying (default: 2)
|
|
261
|
+
tab_accept = 2 # 1 = Tab accepts directly, 2 = Tab opens menu first (default)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Inline suggestions are enabled by default. Set `enabled = false` to disable them entirely (zero performance overhead when disabled).
|
|
265
|
+
|
|
213
266
|
## Quick Start
|
|
214
267
|
|
|
215
268
|
```bash
|
|
269
|
+
# Check your setup
|
|
270
|
+
copa doctor
|
|
271
|
+
|
|
216
272
|
# Import your shell history
|
|
217
273
|
copa sync
|
|
218
274
|
|
|
@@ -222,15 +278,24 @@ copa add "adb shell cmd bluetooth_manager enable" -d "Enable Bluetooth" -g bluet
|
|
|
222
278
|
# Add a command with flag documentation
|
|
223
279
|
copa add "flash_all" -d "Flash AOSP build" -f "--wipe: Wipe userdata" -f "-v: Verbose"
|
|
224
280
|
|
|
225
|
-
#
|
|
226
|
-
copa
|
|
281
|
+
# Pin your most important commands to the top
|
|
282
|
+
copa pin 42
|
|
283
|
+
|
|
284
|
+
# Edit a command's metadata
|
|
285
|
+
copa edit 42 -d "New description" -g mygroup
|
|
227
286
|
|
|
228
287
|
# List top commands by score
|
|
229
288
|
copa list
|
|
230
289
|
|
|
290
|
+
# List as JSON (for scripting)
|
|
291
|
+
copa list --json
|
|
292
|
+
|
|
231
293
|
# Search by keyword
|
|
232
294
|
copa search bluetooth
|
|
233
295
|
|
|
296
|
+
# Create a .copa file from a group (or scaffold an empty one)
|
|
297
|
+
copa create -g bluetooth
|
|
298
|
+
|
|
234
299
|
# Auto-promote frequent commands from history
|
|
235
300
|
copa evolve -k 20
|
|
236
301
|
|
|
@@ -356,7 +421,7 @@ copa create -g bluetooth
|
|
|
356
421
|
copa share export bluetooth -o bluetooth.copa
|
|
357
422
|
```
|
|
358
423
|
|
|
359
|
-
Share it with your team (via git,
|
|
424
|
+
Share it with your team (via git, Slack, or any file share):
|
|
360
425
|
|
|
361
426
|
```bash
|
|
362
427
|
copa share load bluetooth.copa
|
|
@@ -364,6 +429,27 @@ copa share load /path/to/team/commands.copa
|
|
|
364
429
|
copa share sync /path/to/team/copa-files/
|
|
365
430
|
```
|
|
366
431
|
|
|
432
|
+
### Example shared sets
|
|
433
|
+
|
|
434
|
+
Copa ships with ready-to-use `.copa` files in the [`examples/`](examples/) directory:
|
|
435
|
+
|
|
436
|
+
| File | Description |
|
|
437
|
+
|------|-------------|
|
|
438
|
+
| [`git.copa`](examples/git.copa) | Essential Git commands |
|
|
439
|
+
| [`docker.copa`](examples/docker.copa) | Docker container management |
|
|
440
|
+
| [`python-dev.copa`](examples/python-dev.copa) | Python development workflow |
|
|
441
|
+
| [`network.copa`](examples/network.copa) | Network diagnostics |
|
|
442
|
+
| [`adb.copa`](examples/adb.copa) | Android Debug Bridge |
|
|
443
|
+
| [`k8s.copa`](examples/k8s.copa) | Kubernetes cluster management |
|
|
444
|
+
| [`sysadmin.copa`](examples/sysadmin.copa) | System administration |
|
|
445
|
+
|
|
446
|
+
Load any of them:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
copa share load examples/git.copa
|
|
450
|
+
copa share load examples/docker.copa
|
|
451
|
+
```
|
|
452
|
+
|
|
367
453
|
### Filtering by shared set
|
|
368
454
|
|
|
369
455
|
Once you've loaded shared sets, you can scope commands to just that set:
|
|
@@ -480,6 +566,41 @@ toggle_header = "ctrl-h" # show/hide key hints
|
|
|
480
566
|
[completion]
|
|
481
567
|
mode = "fallback" # fallback | hybrid | always | never
|
|
482
568
|
branding = true # show "Copa history" group header
|
|
569
|
+
|
|
570
|
+
# Inline suggestions (ghost text)
|
|
571
|
+
[suggest]
|
|
572
|
+
enabled = true # set to false to disable
|
|
573
|
+
min_length = 2 # minimum chars before querying
|
|
574
|
+
tab_accept = 2 # 1 = accept directly, 2 = open menu first
|
|
575
|
+
|
|
576
|
+
# Composition key behavior (continue vs close)
|
|
577
|
+
# "continue" keys re-open fzf so you can chain another command
|
|
578
|
+
# All other composition keys close fzf immediately
|
|
579
|
+
[composition]
|
|
580
|
+
continue = ["pipe", "chain", "redirect"] # default: |, &&, > re-open fzf
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Composition key behavior
|
|
584
|
+
|
|
585
|
+
When you press a composition key (like Ctrl-A for `&&`), Copa can either **close fzf** (placing the command + operator in your prompt) or **continue** (appending the operator and re-opening fzf so you can select the next command to chain).
|
|
586
|
+
|
|
587
|
+
By default, "connector" operators re-open fzf:
|
|
588
|
+
- `pipe` (`|`) — continue
|
|
589
|
+
- `chain` (`&&`) — continue
|
|
590
|
+
- `redirect` (`>`) — continue
|
|
591
|
+
|
|
592
|
+
And "terminal" operators close fzf:
|
|
593
|
+
- `background` (`&`) — close
|
|
594
|
+
- `merge_output` (`2>&1`) — close
|
|
595
|
+
- `suppress` (`2>/dev/null`) — close
|
|
596
|
+
|
|
597
|
+
When chaining, the prompt shows your accumulated command: `copa [git pull && ]>`.
|
|
598
|
+
|
|
599
|
+
To revert to the old behavior (all keys close immediately), set:
|
|
600
|
+
|
|
601
|
+
```toml
|
|
602
|
+
[composition]
|
|
603
|
+
continue = []
|
|
483
604
|
```
|
|
484
605
|
|
|
485
606
|
## CLI Reference
|
|
@@ -487,11 +608,15 @@ branding = true # show "Copa history" group header
|
|
|
487
608
|
| Command | Purpose |
|
|
488
609
|
|---------|---------|
|
|
489
610
|
| `copa add "cmd" -d "desc" -g group -f "flag: desc"` | Save a command (with optional flags) |
|
|
490
|
-
| `copa
|
|
491
|
-
| `copa list [-g group] [-s source] [--set name]` | List by score |
|
|
492
|
-
| `copa search "query" [-g group] [-s source] [--set name]` | FTS search |
|
|
611
|
+
| `copa edit ID [-d desc] [-g group] [-f flags] [--pin]` | Edit a command's metadata |
|
|
493
612
|
| `copa remove ID` | Remove a command |
|
|
613
|
+
| `copa pin ID` | Pin a command to the top |
|
|
614
|
+
| `copa unpin ID` | Unpin a command |
|
|
615
|
+
| `copa list [-g group] [-s source] [--set name] [--json]` | List by score |
|
|
616
|
+
| `copa search "query" [-g group] [--set name] [--json]` | FTS search |
|
|
617
|
+
| `copa create -g group [-o file]` | Create a .copa file from a group |
|
|
494
618
|
| `copa stats` | Usage statistics |
|
|
619
|
+
| `copa doctor` | Check setup and diagnose issues |
|
|
495
620
|
| `copa sync` | Import from zsh history |
|
|
496
621
|
| `copa scan [--dir path]` | Import script metadata from $PATH |
|
|
497
622
|
| `copa evolve [-k 20] [--auto]` | Auto-add frequent commands (with optional LLM descriptions) |
|
|
@@ -503,6 +628,7 @@ branding = true # show "Copa history" group header
|
|
|
503
628
|
| `copa share list` | List shared sets |
|
|
504
629
|
| `copa share sync DIR` | Sync .copa files from dir |
|
|
505
630
|
| `copa import FILE [-g group]` | Import commands from markdown |
|
|
631
|
+
| `copa uninstall` | Remove Copa data and show cleanup steps |
|
|
506
632
|
|
|
507
633
|
## How Scoring Works
|
|
508
634
|
|
|
@@ -22,11 +22,14 @@ Copa tracks the commands you run, ranks them by frequency and recency, and gives
|
|
|
22
22
|
- **MCP server** — expose your commands to Claude Code (or any MCP client)
|
|
23
23
|
- **Zero latency** — precmd hook records usage in the background
|
|
24
24
|
|
|
25
|
+
> **Note:** Copa requires **zsh**. It is not compatible with bash, fish, or PowerShell.
|
|
26
|
+
|
|
25
27
|
## Install
|
|
26
28
|
|
|
27
29
|
### Prerequisites
|
|
28
30
|
|
|
29
31
|
- **Python 3.12+**
|
|
32
|
+
- **zsh** — Copa's shell integration (precmd hooks, ZLE widgets, inline suggestions) is zsh-only
|
|
30
33
|
- **fzf** — required for Ctrl+R command palette
|
|
31
34
|
|
|
32
35
|
```bash
|
|
@@ -180,9 +183,62 @@ This works automatically once `copa.zsh` is sourced — no extra setup needed. T
|
|
|
180
183
|
|
|
181
184
|
Copa's own CLI completions (`copa li<TAB>` → `list`) continue to work as before via Click's built-in completion.
|
|
182
185
|
|
|
186
|
+
## Inline Suggestions (Ghost Text)
|
|
187
|
+
|
|
188
|
+
Copa shows grey ghost text after your cursor as you type — the best matching command from your database, ranked by frequency and recency. This works like fish shell's autosuggestions or zsh-autosuggestions, with zero plugin dependencies.
|
|
189
|
+
|
|
190
|
+
### How it works
|
|
191
|
+
|
|
192
|
+
As you type, Copa queries its database for commands starting with your current input and displays the highest-scored match as dim grey text after the cursor. The suggestion updates on every keystroke.
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
$ git pu█sh origin main ← grey ghost text
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Keybindings
|
|
199
|
+
|
|
200
|
+
| Key | Suggestion showing | No suggestion |
|
|
201
|
+
|-----|-------------------|---------------|
|
|
202
|
+
| Type chars | Insert char, re-fetch suggestion | Insert char, fetch suggestion |
|
|
203
|
+
| Backspace | Delete char, **latch** (suppress suggestions) | Delete char normally |
|
|
204
|
+
| **Tab** | `tab_accept=1`: accept full suggestion. `tab_accept=2` (default): first Tab highlights suggestion (cyan), second Tab accepts | If latched: unlatch + re-fetch suggestion. Else: normal tab completion |
|
|
205
|
+
| **Down** | Highlight suggestion (enter confirming state) | History navigation |
|
|
206
|
+
| **Right arrow** | Accept one word, re-fetch | Move cursor right |
|
|
207
|
+
| Enter | Clear suggestion, execute | Execute |
|
|
208
|
+
| Esc | Dismiss suggestion | Normal Esc |
|
|
209
|
+
| Up | Clear suggestion, navigate history | History navigation |
|
|
210
|
+
| Ctrl+R | Clear suggestion, open fzf | Open fzf |
|
|
211
|
+
|
|
212
|
+
### Tab accept mode
|
|
213
|
+
|
|
214
|
+
By default (`tab_accept = 2`), pressing Tab when a suggestion is showing highlights the ghost text (changes from dim grey to cyan/bold) to indicate it's ready to be accepted. Pressing Tab again accepts it into the buffer. Pressing Esc reverts to dim grey without accepting. This two-step flow gives you a visual confirmation before committing.
|
|
215
|
+
|
|
216
|
+
Set `tab_accept = 1` to restore the old behavior where a single Tab directly accepts the suggestion.
|
|
217
|
+
|
|
218
|
+
### Backspace latch
|
|
219
|
+
|
|
220
|
+
Pressing Backspace clears the current suggestion and **latches** — suppresses further suggestions while you edit. This prevents suggestions from re-appearing as you retype after correcting a mistake. Ctrl+W (backward-kill-word) also latches.
|
|
221
|
+
|
|
222
|
+
Press **Tab** to unlatch and re-enable suggestions. The next new prompt (Enter) also resets the latch automatically.
|
|
223
|
+
|
|
224
|
+
### Configuration
|
|
225
|
+
|
|
226
|
+
```toml
|
|
227
|
+
# ~/.copa/config.toml
|
|
228
|
+
[suggest]
|
|
229
|
+
enabled = true # set to false to disable inline suggestions
|
|
230
|
+
min_length = 2 # minimum characters before querying (default: 2)
|
|
231
|
+
tab_accept = 2 # 1 = Tab accepts directly, 2 = Tab opens menu first (default)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Inline suggestions are enabled by default. Set `enabled = false` to disable them entirely (zero performance overhead when disabled).
|
|
235
|
+
|
|
183
236
|
## Quick Start
|
|
184
237
|
|
|
185
238
|
```bash
|
|
239
|
+
# Check your setup
|
|
240
|
+
copa doctor
|
|
241
|
+
|
|
186
242
|
# Import your shell history
|
|
187
243
|
copa sync
|
|
188
244
|
|
|
@@ -192,15 +248,24 @@ copa add "adb shell cmd bluetooth_manager enable" -d "Enable Bluetooth" -g bluet
|
|
|
192
248
|
# Add a command with flag documentation
|
|
193
249
|
copa add "flash_all" -d "Flash AOSP build" -f "--wipe: Wipe userdata" -f "-v: Verbose"
|
|
194
250
|
|
|
195
|
-
#
|
|
196
|
-
copa
|
|
251
|
+
# Pin your most important commands to the top
|
|
252
|
+
copa pin 42
|
|
253
|
+
|
|
254
|
+
# Edit a command's metadata
|
|
255
|
+
copa edit 42 -d "New description" -g mygroup
|
|
197
256
|
|
|
198
257
|
# List top commands by score
|
|
199
258
|
copa list
|
|
200
259
|
|
|
260
|
+
# List as JSON (for scripting)
|
|
261
|
+
copa list --json
|
|
262
|
+
|
|
201
263
|
# Search by keyword
|
|
202
264
|
copa search bluetooth
|
|
203
265
|
|
|
266
|
+
# Create a .copa file from a group (or scaffold an empty one)
|
|
267
|
+
copa create -g bluetooth
|
|
268
|
+
|
|
204
269
|
# Auto-promote frequent commands from history
|
|
205
270
|
copa evolve -k 20
|
|
206
271
|
|
|
@@ -326,7 +391,7 @@ copa create -g bluetooth
|
|
|
326
391
|
copa share export bluetooth -o bluetooth.copa
|
|
327
392
|
```
|
|
328
393
|
|
|
329
|
-
Share it with your team (via git,
|
|
394
|
+
Share it with your team (via git, Slack, or any file share):
|
|
330
395
|
|
|
331
396
|
```bash
|
|
332
397
|
copa share load bluetooth.copa
|
|
@@ -334,6 +399,27 @@ copa share load /path/to/team/commands.copa
|
|
|
334
399
|
copa share sync /path/to/team/copa-files/
|
|
335
400
|
```
|
|
336
401
|
|
|
402
|
+
### Example shared sets
|
|
403
|
+
|
|
404
|
+
Copa ships with ready-to-use `.copa` files in the [`examples/`](examples/) directory:
|
|
405
|
+
|
|
406
|
+
| File | Description |
|
|
407
|
+
|------|-------------|
|
|
408
|
+
| [`git.copa`](examples/git.copa) | Essential Git commands |
|
|
409
|
+
| [`docker.copa`](examples/docker.copa) | Docker container management |
|
|
410
|
+
| [`python-dev.copa`](examples/python-dev.copa) | Python development workflow |
|
|
411
|
+
| [`network.copa`](examples/network.copa) | Network diagnostics |
|
|
412
|
+
| [`adb.copa`](examples/adb.copa) | Android Debug Bridge |
|
|
413
|
+
| [`k8s.copa`](examples/k8s.copa) | Kubernetes cluster management |
|
|
414
|
+
| [`sysadmin.copa`](examples/sysadmin.copa) | System administration |
|
|
415
|
+
|
|
416
|
+
Load any of them:
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
copa share load examples/git.copa
|
|
420
|
+
copa share load examples/docker.copa
|
|
421
|
+
```
|
|
422
|
+
|
|
337
423
|
### Filtering by shared set
|
|
338
424
|
|
|
339
425
|
Once you've loaded shared sets, you can scope commands to just that set:
|
|
@@ -450,6 +536,41 @@ toggle_header = "ctrl-h" # show/hide key hints
|
|
|
450
536
|
[completion]
|
|
451
537
|
mode = "fallback" # fallback | hybrid | always | never
|
|
452
538
|
branding = true # show "Copa history" group header
|
|
539
|
+
|
|
540
|
+
# Inline suggestions (ghost text)
|
|
541
|
+
[suggest]
|
|
542
|
+
enabled = true # set to false to disable
|
|
543
|
+
min_length = 2 # minimum chars before querying
|
|
544
|
+
tab_accept = 2 # 1 = accept directly, 2 = open menu first
|
|
545
|
+
|
|
546
|
+
# Composition key behavior (continue vs close)
|
|
547
|
+
# "continue" keys re-open fzf so you can chain another command
|
|
548
|
+
# All other composition keys close fzf immediately
|
|
549
|
+
[composition]
|
|
550
|
+
continue = ["pipe", "chain", "redirect"] # default: |, &&, > re-open fzf
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Composition key behavior
|
|
554
|
+
|
|
555
|
+
When you press a composition key (like Ctrl-A for `&&`), Copa can either **close fzf** (placing the command + operator in your prompt) or **continue** (appending the operator and re-opening fzf so you can select the next command to chain).
|
|
556
|
+
|
|
557
|
+
By default, "connector" operators re-open fzf:
|
|
558
|
+
- `pipe` (`|`) — continue
|
|
559
|
+
- `chain` (`&&`) — continue
|
|
560
|
+
- `redirect` (`>`) — continue
|
|
561
|
+
|
|
562
|
+
And "terminal" operators close fzf:
|
|
563
|
+
- `background` (`&`) — close
|
|
564
|
+
- `merge_output` (`2>&1`) — close
|
|
565
|
+
- `suppress` (`2>/dev/null`) — close
|
|
566
|
+
|
|
567
|
+
When chaining, the prompt shows your accumulated command: `copa [git pull && ]>`.
|
|
568
|
+
|
|
569
|
+
To revert to the old behavior (all keys close immediately), set:
|
|
570
|
+
|
|
571
|
+
```toml
|
|
572
|
+
[composition]
|
|
573
|
+
continue = []
|
|
453
574
|
```
|
|
454
575
|
|
|
455
576
|
## CLI Reference
|
|
@@ -457,11 +578,15 @@ branding = true # show "Copa history" group header
|
|
|
457
578
|
| Command | Purpose |
|
|
458
579
|
|---------|---------|
|
|
459
580
|
| `copa add "cmd" -d "desc" -g group -f "flag: desc"` | Save a command (with optional flags) |
|
|
460
|
-
| `copa
|
|
461
|
-
| `copa list [-g group] [-s source] [--set name]` | List by score |
|
|
462
|
-
| `copa search "query" [-g group] [-s source] [--set name]` | FTS search |
|
|
581
|
+
| `copa edit ID [-d desc] [-g group] [-f flags] [--pin]` | Edit a command's metadata |
|
|
463
582
|
| `copa remove ID` | Remove a command |
|
|
583
|
+
| `copa pin ID` | Pin a command to the top |
|
|
584
|
+
| `copa unpin ID` | Unpin a command |
|
|
585
|
+
| `copa list [-g group] [-s source] [--set name] [--json]` | List by score |
|
|
586
|
+
| `copa search "query" [-g group] [--set name] [--json]` | FTS search |
|
|
587
|
+
| `copa create -g group [-o file]` | Create a .copa file from a group |
|
|
464
588
|
| `copa stats` | Usage statistics |
|
|
589
|
+
| `copa doctor` | Check setup and diagnose issues |
|
|
465
590
|
| `copa sync` | Import from zsh history |
|
|
466
591
|
| `copa scan [--dir path]` | Import script metadata from $PATH |
|
|
467
592
|
| `copa evolve [-k 20] [--auto]` | Auto-add frequent commands (with optional LLM descriptions) |
|
|
@@ -473,6 +598,7 @@ branding = true # show "Copa history" group header
|
|
|
473
598
|
| `copa share list` | List shared sets |
|
|
474
599
|
| `copa share sync DIR` | Sync .copa files from dir |
|
|
475
600
|
| `copa import FILE [-g group]` | Import commands from markdown |
|
|
601
|
+
| `copa uninstall` | Remove Copa data and show cleanup steps |
|
|
476
602
|
|
|
477
603
|
## How Scoring Works
|
|
478
604
|
|
|
@@ -2,14 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
5
7
|
import sys
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
|
|
8
10
|
import click
|
|
9
11
|
|
|
10
12
|
from .cli_common import complete_group, complete_shared_set, complete_source, get_db
|
|
13
|
+
from .models import Command
|
|
11
14
|
from .scoring import rank_commands
|
|
12
15
|
|
|
16
|
+
|
|
17
|
+
def _cmd_to_json(cmd: Command) -> dict:
|
|
18
|
+
"""Convert a Command to a JSON-serializable dict."""
|
|
19
|
+
d: dict = {
|
|
20
|
+
"id": cmd.id,
|
|
21
|
+
"command": cmd.command,
|
|
22
|
+
"description": cmd.description,
|
|
23
|
+
"frequency": cmd.frequency,
|
|
24
|
+
"score": round(cmd.score, 2),
|
|
25
|
+
"source": cmd.source,
|
|
26
|
+
}
|
|
27
|
+
if cmd.group_name:
|
|
28
|
+
d["group"] = cmd.group_name
|
|
29
|
+
if cmd.shared_set:
|
|
30
|
+
d["shared_set"] = cmd.shared_set
|
|
31
|
+
if cmd.is_pinned:
|
|
32
|
+
d["pinned"] = True
|
|
33
|
+
if cmd.tags:
|
|
34
|
+
d["tags"] = cmd.tags
|
|
35
|
+
if cmd.flags:
|
|
36
|
+
d["flags"] = cmd.flags
|
|
37
|
+
return d
|
|
38
|
+
|
|
39
|
+
|
|
13
40
|
# --- Main group ---
|
|
14
41
|
|
|
15
42
|
|
|
@@ -127,7 +154,15 @@ def add(command: str, description: str, group: str | None, tag: tuple[str, ...],
|
|
|
127
154
|
@click.option("-s", "--source", default=None, help="Filter by source.", shell_complete=complete_source)
|
|
128
155
|
@click.option("--set", "shared_set", default=None, help="Filter by shared set.", shell_complete=complete_shared_set)
|
|
129
156
|
@click.option("--needs-desc", is_flag=True, help="Show only commands needing description.")
|
|
130
|
-
|
|
157
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
|
|
158
|
+
def list_cmd(
|
|
159
|
+
group: str | None,
|
|
160
|
+
limit: int,
|
|
161
|
+
source: str | None,
|
|
162
|
+
shared_set: str | None,
|
|
163
|
+
needs_desc: bool,
|
|
164
|
+
as_json: bool,
|
|
165
|
+
):
|
|
131
166
|
"""List commands ranked by score."""
|
|
132
167
|
db = get_db()
|
|
133
168
|
commands = db.list_commands(
|
|
@@ -139,7 +174,14 @@ def list_cmd(group: str | None, limit: int, source: str | None, shared_set: str
|
|
|
139
174
|
)
|
|
140
175
|
ranked = rank_commands(commands)
|
|
141
176
|
if not ranked:
|
|
142
|
-
|
|
177
|
+
if as_json:
|
|
178
|
+
click.echo("[]")
|
|
179
|
+
else:
|
|
180
|
+
click.echo("No commands found.")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
if as_json:
|
|
184
|
+
click.echo(json.dumps([_cmd_to_json(c) for c in ranked], indent=2))
|
|
143
185
|
return
|
|
144
186
|
|
|
145
187
|
for cmd in ranked:
|
|
@@ -170,13 +212,21 @@ def list_cmd(group: str | None, limit: int, source: str | None, shared_set: str
|
|
|
170
212
|
@click.option("-s", "--source", default=None, help="Filter by source.", shell_complete=complete_source)
|
|
171
213
|
@click.option("--set", "shared_set", default=None, help="Filter by shared set.", shell_complete=complete_shared_set)
|
|
172
214
|
@click.option("-n", "--limit", default=20, help="Max results.")
|
|
173
|
-
|
|
215
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
|
|
216
|
+
def search(query: str, group: str | None, source: str | None, shared_set: str | None, limit: int, as_json: bool):
|
|
174
217
|
"""Search commands by keyword (FTS)."""
|
|
175
218
|
db = get_db()
|
|
176
219
|
commands = db.search_commands(query, group_name=group, source=source, shared_set=shared_set, limit=limit)
|
|
177
220
|
ranked = rank_commands(commands)
|
|
178
221
|
if not ranked:
|
|
179
|
-
|
|
222
|
+
if as_json:
|
|
223
|
+
click.echo("[]")
|
|
224
|
+
else:
|
|
225
|
+
click.echo(f"No commands matching '{query}'.")
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
if as_json:
|
|
229
|
+
click.echo(json.dumps([_cmd_to_json(c) for c in ranked], indent=2))
|
|
180
230
|
return
|
|
181
231
|
|
|
182
232
|
for cmd in ranked:
|
|
@@ -261,6 +311,148 @@ def scan(directory: str | None):
|
|
|
261
311
|
click.echo(f"Scanned: {added} scripts added.")
|
|
262
312
|
|
|
263
313
|
|
|
314
|
+
# --- pin / unpin ---
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@cli.command()
|
|
318
|
+
@click.argument("cmd_id", type=int)
|
|
319
|
+
def pin(cmd_id: int):
|
|
320
|
+
"""Pin a command so it always appears at the top."""
|
|
321
|
+
db = get_db()
|
|
322
|
+
cmd = db.get_command(cmd_id)
|
|
323
|
+
if not cmd:
|
|
324
|
+
click.echo(f"Command {cmd_id} not found.", err=True)
|
|
325
|
+
sys.exit(1)
|
|
326
|
+
db.pin_command(cmd_id, True)
|
|
327
|
+
click.echo(f"Pinned [{cmd_id}]: {cmd.command}")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@cli.command()
|
|
331
|
+
@click.argument("cmd_id", type=int)
|
|
332
|
+
def unpin(cmd_id: int):
|
|
333
|
+
"""Unpin a command."""
|
|
334
|
+
db = get_db()
|
|
335
|
+
cmd = db.get_command(cmd_id)
|
|
336
|
+
if not cmd:
|
|
337
|
+
click.echo(f"Command {cmd_id} not found.", err=True)
|
|
338
|
+
sys.exit(1)
|
|
339
|
+
db.pin_command(cmd_id, False)
|
|
340
|
+
click.echo(f"Unpinned [{cmd_id}]: {cmd.command}")
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# --- edit ---
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@cli.command()
|
|
347
|
+
@click.argument("cmd_id", type=int)
|
|
348
|
+
@click.option("-d", "--description", default=None, help="New description.")
|
|
349
|
+
@click.option("-g", "--group", default=None, help="New group name (use '' to clear).", shell_complete=complete_group)
|
|
350
|
+
@click.option("-f", "--flag", multiple=True, help="Flag docs as 'flag: description' (repeatable, replaces all flags).")
|
|
351
|
+
@click.option("-p", "--pin/--no-pin", default=None, help="Pin or unpin the command.")
|
|
352
|
+
def edit(cmd_id: int, description: str | None, group: str | None, flag: tuple[str, ...], pin: bool | None):
|
|
353
|
+
"""Edit a command's metadata by ID."""
|
|
354
|
+
db = get_db()
|
|
355
|
+
cmd = db.get_command(cmd_id)
|
|
356
|
+
if not cmd:
|
|
357
|
+
click.echo(f"Command {cmd_id} not found.", err=True)
|
|
358
|
+
sys.exit(1)
|
|
359
|
+
|
|
360
|
+
changes = []
|
|
361
|
+
if description is not None:
|
|
362
|
+
db.update_description(cmd_id, description)
|
|
363
|
+
changes.append(f"description: {description}")
|
|
364
|
+
if group is not None:
|
|
365
|
+
group_name = group if group else None
|
|
366
|
+
db.update_group(cmd_id, group_name)
|
|
367
|
+
changes.append(f"group: {group_name or '(none)'}")
|
|
368
|
+
if flag:
|
|
369
|
+
flags: dict[str, str] = {}
|
|
370
|
+
for f in flag:
|
|
371
|
+
parts = f.split(":", 1)
|
|
372
|
+
flag_name = parts[0].strip()
|
|
373
|
+
flag_desc = parts[1].strip() if len(parts) > 1 else ""
|
|
374
|
+
flags[flag_name] = flag_desc
|
|
375
|
+
db.update_flags(cmd_id, flags)
|
|
376
|
+
changes.append(f"flags: {len(flags)} documented")
|
|
377
|
+
if pin is not None:
|
|
378
|
+
db.pin_command(cmd_id, pin)
|
|
379
|
+
changes.append("pinned" if pin else "unpinned")
|
|
380
|
+
|
|
381
|
+
if not changes:
|
|
382
|
+
click.echo(f"[{cmd_id}] {cmd.command} — nothing to change (use -d, -g, -f, or --pin)")
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
click.echo(f"Updated [{cmd_id}]: {cmd.command}")
|
|
386
|
+
for c in changes:
|
|
387
|
+
click.echo(f" → {c}")
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# --- doctor ---
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@cli.command()
|
|
394
|
+
def doctor():
|
|
395
|
+
"""Check Copa setup and diagnose common issues."""
|
|
396
|
+
ok_mark = click.style("OK", fg="green")
|
|
397
|
+
warn_mark = click.style("!!", fg="yellow")
|
|
398
|
+
fail_mark = click.style("FAIL", fg="red")
|
|
399
|
+
|
|
400
|
+
click.echo("Copa Doctor\n")
|
|
401
|
+
|
|
402
|
+
# 1. Database
|
|
403
|
+
db_path = Path.home() / ".copa" / "copa.db"
|
|
404
|
+
if db_path.is_file():
|
|
405
|
+
size = db_path.stat().st_size
|
|
406
|
+
click.echo(f" [{ok_mark}] Database: {db_path} ({size:,} bytes)")
|
|
407
|
+
db = get_db()
|
|
408
|
+
s = db.get_stats()
|
|
409
|
+
click.echo(f" {s['total_commands']} commands, {s['total_groups']} groups, {s['shared_sets']} shared sets")
|
|
410
|
+
else:
|
|
411
|
+
click.echo(f" [{fail_mark}] Database: not found at {db_path}")
|
|
412
|
+
click.echo(" Run: copa _init")
|
|
413
|
+
|
|
414
|
+
# 2. fzf
|
|
415
|
+
if shutil.which("fzf"):
|
|
416
|
+
click.echo(f" [{ok_mark}] fzf: installed")
|
|
417
|
+
else:
|
|
418
|
+
click.echo(f" [{fail_mark}] fzf: not found")
|
|
419
|
+
click.echo(" Install: brew install fzf")
|
|
420
|
+
|
|
421
|
+
# 3. Shell integration
|
|
422
|
+
zshrc = Path.home() / ".zshrc"
|
|
423
|
+
if zshrc.is_file() and "copa init zsh" in zshrc.read_text():
|
|
424
|
+
click.echo(f" [{ok_mark}] Shell integration: found in ~/.zshrc")
|
|
425
|
+
else:
|
|
426
|
+
click.echo(f" [{warn_mark}] Shell integration: not detected in ~/.zshrc")
|
|
427
|
+
click.echo(' Add: eval "$(copa init zsh)"')
|
|
428
|
+
|
|
429
|
+
# 4. LLM backend
|
|
430
|
+
if db_path.is_file():
|
|
431
|
+
db = get_db()
|
|
432
|
+
backend = db.get_meta("llm_backend")
|
|
433
|
+
if backend:
|
|
434
|
+
click.echo(f" [{ok_mark}] LLM backend: {backend}")
|
|
435
|
+
if backend == "ollama":
|
|
436
|
+
model = db.get_meta("ollama_model") or "llama3.2:3b"
|
|
437
|
+
click.echo(f" Model: {model}")
|
|
438
|
+
else:
|
|
439
|
+
click.echo(f" [{warn_mark}] LLM backend: not configured")
|
|
440
|
+
click.echo(" Run: copa configure")
|
|
441
|
+
|
|
442
|
+
# 5. Completion mode
|
|
443
|
+
config_path = Path.home() / ".copa" / "config.toml"
|
|
444
|
+
if config_path.is_file():
|
|
445
|
+
from .config import load_config
|
|
446
|
+
|
|
447
|
+
cfg = load_config(config_path)
|
|
448
|
+
mode = cfg.get("_completion_mode", "fallback")
|
|
449
|
+
click.echo(f" [{ok_mark}] Completion mode: {mode}")
|
|
450
|
+
else:
|
|
451
|
+
click.echo(f" [{ok_mark}] Completion mode: fallback (default)")
|
|
452
|
+
|
|
453
|
+
click.echo()
|
|
454
|
+
|
|
455
|
+
|
|
264
456
|
# --- Register extracted command modules ---
|
|
265
457
|
|
|
266
458
|
from . import cli_internal, cli_llm, cli_share
|