copa-cli 0.7.1__tar.gz → 0.8.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.
Files changed (37) hide show
  1. {copa_cli-0.7.1 → copa_cli-0.8.0}/PKG-INFO +36 -1
  2. {copa_cli-0.7.1 → copa_cli-0.8.0}/README.md +35 -0
  3. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/copa.zsh +22 -5
  4. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/db.py +2 -1
  5. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/mcp_server.py +105 -0
  6. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa_cli.egg-info/PKG-INFO +36 -1
  7. {copa_cli-0.7.1 → copa_cli-0.8.0}/pyproject.toml +1 -1
  8. {copa_cli-0.7.1 → copa_cli-0.8.0}/tests/test_modal.py +36 -0
  9. {copa_cli-0.7.1 → copa_cli-0.8.0}/LICENSE +0 -0
  10. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/__init__.py +0 -0
  11. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/__main__.py +0 -0
  12. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/cli.py +0 -0
  13. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/cli_common.py +0 -0
  14. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/cli_internal.py +0 -0
  15. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/cli_llm.py +0 -0
  16. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/cli_share.py +0 -0
  17. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/config.py +0 -0
  18. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/evolve.py +0 -0
  19. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/fzf.py +0 -0
  20. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/history.py +0 -0
  21. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/llm.py +0 -0
  22. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/models.py +0 -0
  23. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/scanner.py +0 -0
  24. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/scoring.py +0 -0
  25. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa/sharing.py +0 -0
  26. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa_cli.egg-info/SOURCES.txt +0 -0
  27. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa_cli.egg-info/dependency_links.txt +0 -0
  28. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa_cli.egg-info/entry_points.txt +0 -0
  29. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa_cli.egg-info/requires.txt +0 -0
  30. {copa_cli-0.7.1 → copa_cli-0.8.0}/copa_cli.egg-info/top_level.txt +0 -0
  31. {copa_cli-0.7.1 → copa_cli-0.8.0}/setup.cfg +0 -0
  32. {copa_cli-0.7.1 → copa_cli-0.8.0}/tests/test_cli_and_sharing.py +0 -0
  33. {copa_cli-0.7.1 → copa_cli-0.8.0}/tests/test_db.py +0 -0
  34. {copa_cli-0.7.1 → copa_cli-0.8.0}/tests/test_fzf.py +0 -0
  35. {copa_cli-0.7.1 → copa_cli-0.8.0}/tests/test_models.py +0 -0
  36. {copa_cli-0.7.1 → copa_cli-0.8.0}/tests/test_polish.py +0 -0
  37. {copa_cli-0.7.1 → copa_cli-0.8.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.7.1
3
+ Version: 0.8.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
@@ -36,6 +36,8 @@ Dynamic: license-file
36
36
 
37
37
  Copa tracks the commands you run, ranks them by frequency and recency, and gives you instant fuzzy search via fzf. Think of it as a smart, searchable, shareable upgrade to shell history.
38
38
 
39
+ ![Copa Setup](demos/01-setup.gif)
40
+
39
41
  ## Features
40
42
 
41
43
  - **Smart ranking** — commands scored by `2*log(1+freq) + 8*0.5^(age/3d)`, so frequent *and* recent commands float to the top
@@ -134,6 +136,8 @@ copa sync
134
136
 
135
137
  Once shell integration is sourced, pressing **Ctrl+R** opens an fzf-powered command palette instead of the default zsh reverse search. This is Copa's primary interface.
136
138
 
139
+ ![Ctrl+R Palette](demos/04-ctrl-r-palette.gif)
140
+
137
141
  ### What you see
138
142
 
139
143
  Copa pipes every tracked command into fzf with aligned columns:
@@ -173,6 +177,8 @@ While the fzf palette is open, these keys are available:
173
177
  | **Ctrl+/** | Append `2>/dev/null` | Suppress stderr |
174
178
  | **Ctrl+S** | Scope by group | Opens inline group list — Enter filters to that group, ESC returns to all |
175
179
  | **Ctrl+G** | Assign group | Opens inline group list — Enter assigns the group to the highlighted command |
180
+
181
+ ![Groups and Scoping](demos/05-groups-and-scoping.gif)
176
182
  | **Ctrl+N** | Cycle group | Cycles through groups: (all) → group1 → group2 → ... → (all) |
177
183
  | **Ctrl+D** | Describe | Generate/edit a description using LLM (with tty-aware input) |
178
184
  | **Ctrl+F** | Edit flags | Add flag documentation to the highlighted command |
@@ -184,6 +190,8 @@ Keybindings are configurable via `~/.copa/config.toml`. See [Configuration](#con
184
190
 
185
191
  ### Select mode (bulk operations)
186
192
 
193
+ ![Bulk Operations](demos/06-bulk-operations.gif)
194
+
187
195
  Press **Ctrl+B** from the Ctrl+R palette to enter **select mode**. This opens a new fzf view with multi-select enabled:
188
196
 
189
197
  - **Tab** toggles selection on individual commands
@@ -220,10 +228,14 @@ Selecting a command places it directly into your shell prompt (without executing
220
228
 
221
229
  ## Tab Completion
222
230
 
231
+ ![Tab Menu Select](demos/03-tab-menu-select.gif)
232
+
223
233
  Copa supplements zsh's built-in tab completion for **any** command — not just Copa's own CLI. Once `copa.zsh` is sourced, Copa registers as a completer in zsh's completion system.
224
234
 
225
235
  ### Completion modes
226
236
 
237
+ ![Completion Modes](demos/10-completion-modes.gif)
238
+
227
239
  Copa supports four completion modes, configured via `~/.copa/config.toml`:
228
240
 
229
241
  | Mode | Behavior |
@@ -269,6 +281,10 @@ Copa's own CLI completions (`copa li<TAB>` → `list`) continue to work as befor
269
281
 
270
282
  ## Inline Suggestions (Ghost Text)
271
283
 
284
+ | Tab Mode 1 (direct accept) | Tab Mode 2 (menu select, default) |
285
+ |---|---|
286
+ | ![Tab Mode 1](demos/02a-suggestions-tab-mode1.gif) | ![Tab Mode 2](demos/02b-suggestions-tab-mode2.gif) |
287
+
272
288
  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.
273
289
 
274
290
  ### How it works
@@ -288,6 +304,7 @@ $ git pu█sh origin main ← grey ghost text
288
304
  | **Tab** | `tab_accept=1`: accept full suggestion. `tab_accept=2` (default): open completion menu with suggestion highlighted at top, native completions below | If latched: unlatch + re-fetch suggestion. Else: normal tab completion |
289
305
  | **Down** | Open completion menu with suggestion highlighted at top | History navigation |
290
306
  | **Right arrow** | Accept one word, re-fetch | Move cursor right |
307
+ | **Cmd+Right / End** | Accept full suggestion | Move to end of line |
291
308
  | Enter | Clear suggestion, execute | Execute |
292
309
  | Esc | Dismiss suggestion | Normal Esc |
293
310
  | Up | Clear suggestion, navigate history | History navigation |
@@ -441,6 +458,8 @@ copa describe 42
441
458
 
442
459
  Generates a description for a specific command by ID. Same accept/edit flow as `fix --auto`.
443
460
 
461
+ ![Flags and Describe](demos/09-flags-and-describe.gif)
462
+
444
463
  ## Script Metadata Protocol
445
464
 
446
465
  Copa recognizes `#@` headers in script files (checked in the first 50 lines):
@@ -488,6 +507,8 @@ copa scan --dir ~/bin # scan a specific directory
488
507
 
489
508
  ## Sharing
490
509
 
510
+ ![Sharing](demos/07-sharing.gif)
511
+
491
512
  Export a group as a `.copa` file:
492
513
 
493
514
  ```bash
@@ -622,8 +643,16 @@ Available MCP tools:
622
643
  - `copa_get_stats` — usage statistics
623
644
  - `copa_add_command` — add a command
624
645
  - `copa_update_description` — update a description
646
+ - `copa_delete_command` — delete a command by ID
647
+ - `copa_set_group` — set or change a command's group
648
+ - `copa_update_flags` — update flag documentation for a command
649
+ - `copa_pin_command` — pin or unpin a command
625
650
  - `copa_create_group` — create a group with commands
626
651
  - `copa_bulk_add` — bulk add commands
652
+ - `copa_share_load` — load a .copa file as a shared set
653
+ - `copa_share_list` — list all loaded shared sets
654
+ - `copa_share_remove` — remove a shared set
655
+ - `copa_export_group` — export a group as a .copa file
627
656
 
628
657
  ## Configuration
629
658
 
@@ -671,6 +700,8 @@ continue = ["pipe", "chain", "redirect"] # default: |, &&, > re-open fzf
671
700
 
672
701
  ### Composition key behavior
673
702
 
703
+ ![Composition](demos/08-composition.gif)
704
+
674
705
  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).
675
706
 
676
707
  By default, "connector" operators re-open fzf:
@@ -692,6 +723,10 @@ To revert to the old behavior (all keys close immediately), set:
692
723
  continue = []
693
724
  ```
694
725
 
726
+ ## End-to-End Workflow
727
+
728
+ ![Workflow](demos/11-workflow.gif)
729
+
695
730
  ## CLI Reference
696
731
 
697
732
  | Command | Purpose |
@@ -6,6 +6,8 @@
6
6
 
7
7
  Copa tracks the commands you run, ranks them by frequency and recency, and gives you instant fuzzy search via fzf. Think of it as a smart, searchable, shareable upgrade to shell history.
8
8
 
9
+ ![Copa Setup](demos/01-setup.gif)
10
+
9
11
  ## Features
10
12
 
11
13
  - **Smart ranking** — commands scored by `2*log(1+freq) + 8*0.5^(age/3d)`, so frequent *and* recent commands float to the top
@@ -104,6 +106,8 @@ copa sync
104
106
 
105
107
  Once shell integration is sourced, pressing **Ctrl+R** opens an fzf-powered command palette instead of the default zsh reverse search. This is Copa's primary interface.
106
108
 
109
+ ![Ctrl+R Palette](demos/04-ctrl-r-palette.gif)
110
+
107
111
  ### What you see
108
112
 
109
113
  Copa pipes every tracked command into fzf with aligned columns:
@@ -143,6 +147,8 @@ While the fzf palette is open, these keys are available:
143
147
  | **Ctrl+/** | Append `2>/dev/null` | Suppress stderr |
144
148
  | **Ctrl+S** | Scope by group | Opens inline group list — Enter filters to that group, ESC returns to all |
145
149
  | **Ctrl+G** | Assign group | Opens inline group list — Enter assigns the group to the highlighted command |
150
+
151
+ ![Groups and Scoping](demos/05-groups-and-scoping.gif)
146
152
  | **Ctrl+N** | Cycle group | Cycles through groups: (all) → group1 → group2 → ... → (all) |
147
153
  | **Ctrl+D** | Describe | Generate/edit a description using LLM (with tty-aware input) |
148
154
  | **Ctrl+F** | Edit flags | Add flag documentation to the highlighted command |
@@ -154,6 +160,8 @@ Keybindings are configurable via `~/.copa/config.toml`. See [Configuration](#con
154
160
 
155
161
  ### Select mode (bulk operations)
156
162
 
163
+ ![Bulk Operations](demos/06-bulk-operations.gif)
164
+
157
165
  Press **Ctrl+B** from the Ctrl+R palette to enter **select mode**. This opens a new fzf view with multi-select enabled:
158
166
 
159
167
  - **Tab** toggles selection on individual commands
@@ -190,10 +198,14 @@ Selecting a command places it directly into your shell prompt (without executing
190
198
 
191
199
  ## Tab Completion
192
200
 
201
+ ![Tab Menu Select](demos/03-tab-menu-select.gif)
202
+
193
203
  Copa supplements zsh's built-in tab completion for **any** command — not just Copa's own CLI. Once `copa.zsh` is sourced, Copa registers as a completer in zsh's completion system.
194
204
 
195
205
  ### Completion modes
196
206
 
207
+ ![Completion Modes](demos/10-completion-modes.gif)
208
+
197
209
  Copa supports four completion modes, configured via `~/.copa/config.toml`:
198
210
 
199
211
  | Mode | Behavior |
@@ -239,6 +251,10 @@ Copa's own CLI completions (`copa li<TAB>` → `list`) continue to work as befor
239
251
 
240
252
  ## Inline Suggestions (Ghost Text)
241
253
 
254
+ | Tab Mode 1 (direct accept) | Tab Mode 2 (menu select, default) |
255
+ |---|---|
256
+ | ![Tab Mode 1](demos/02a-suggestions-tab-mode1.gif) | ![Tab Mode 2](demos/02b-suggestions-tab-mode2.gif) |
257
+
242
258
  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.
243
259
 
244
260
  ### How it works
@@ -258,6 +274,7 @@ $ git pu█sh origin main ← grey ghost text
258
274
  | **Tab** | `tab_accept=1`: accept full suggestion. `tab_accept=2` (default): open completion menu with suggestion highlighted at top, native completions below | If latched: unlatch + re-fetch suggestion. Else: normal tab completion |
259
275
  | **Down** | Open completion menu with suggestion highlighted at top | History navigation |
260
276
  | **Right arrow** | Accept one word, re-fetch | Move cursor right |
277
+ | **Cmd+Right / End** | Accept full suggestion | Move to end of line |
261
278
  | Enter | Clear suggestion, execute | Execute |
262
279
  | Esc | Dismiss suggestion | Normal Esc |
263
280
  | Up | Clear suggestion, navigate history | History navigation |
@@ -411,6 +428,8 @@ copa describe 42
411
428
 
412
429
  Generates a description for a specific command by ID. Same accept/edit flow as `fix --auto`.
413
430
 
431
+ ![Flags and Describe](demos/09-flags-and-describe.gif)
432
+
414
433
  ## Script Metadata Protocol
415
434
 
416
435
  Copa recognizes `#@` headers in script files (checked in the first 50 lines):
@@ -458,6 +477,8 @@ copa scan --dir ~/bin # scan a specific directory
458
477
 
459
478
  ## Sharing
460
479
 
480
+ ![Sharing](demos/07-sharing.gif)
481
+
461
482
  Export a group as a `.copa` file:
462
483
 
463
484
  ```bash
@@ -592,8 +613,16 @@ Available MCP tools:
592
613
  - `copa_get_stats` — usage statistics
593
614
  - `copa_add_command` — add a command
594
615
  - `copa_update_description` — update a description
616
+ - `copa_delete_command` — delete a command by ID
617
+ - `copa_set_group` — set or change a command's group
618
+ - `copa_update_flags` — update flag documentation for a command
619
+ - `copa_pin_command` — pin or unpin a command
595
620
  - `copa_create_group` — create a group with commands
596
621
  - `copa_bulk_add` — bulk add commands
622
+ - `copa_share_load` — load a .copa file as a shared set
623
+ - `copa_share_list` — list all loaded shared sets
624
+ - `copa_share_remove` — remove a shared set
625
+ - `copa_export_group` — export a group as a .copa file
597
626
 
598
627
  ## Configuration
599
628
 
@@ -641,6 +670,8 @@ continue = ["pipe", "chain", "redirect"] # default: |, &&, > re-open fzf
641
670
 
642
671
  ### Composition key behavior
643
672
 
673
+ ![Composition](demos/08-composition.gif)
674
+
644
675
  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).
645
676
 
646
677
  By default, "connector" operators re-open fzf:
@@ -662,6 +693,10 @@ To revert to the old behavior (all keys close immediately), set:
662
693
  continue = []
663
694
  ```
664
695
 
696
+ ## End-to-End Workflow
697
+
698
+ ![Workflow](demos/11-workflow.gif)
699
+
665
700
  ## CLI Reference
666
701
 
667
702
  | Command | Purpose |
@@ -45,11 +45,12 @@ eval "$(copa _fzf-config 2>/dev/null)" || {
45
45
 
46
46
  # --- precmd hook: record last command ---
47
47
  _copa_precmd() {
48
+ (( $+commands[copa] )) || return
48
49
  local last_cmd
49
50
  last_cmd="$(fc -ln -1 2>/dev/null)"
50
51
  last_cmd="${last_cmd## }" # strip leading space
51
52
  if [[ -n "$last_cmd" && "$last_cmd" != _copa_* ]]; then
52
- copa _record "$last_cmd" &!
53
+ copa _record "$last_cmd" &! 2>/dev/null
53
54
  fi
54
55
  }
55
56
 
@@ -280,7 +281,7 @@ bindkey '^R' _copa_fzf_widget
280
281
  if ! (( $+functions[compdef] )); then
281
282
  autoload -Uz compinit && compinit -i -C
282
283
  fi
283
- eval "$(copa completion zsh)"
284
+ eval "$(copa completion zsh 2>/dev/null)"
284
285
 
285
286
  # --- Supplemental tab completion from Copa database ---
286
287
  # Mode is controlled by _COPA_COMPLETION_MODE (set via copa _fzf-config):
@@ -340,7 +341,7 @@ _copa_history_complete() {
340
341
  local -a cur
341
342
  zstyle -g cur ':completion:*' completer 2>/dev/null
342
343
  if (( ! ${cur[(Ie)_copa_history_complete]} )); then
343
- zstyle ':completion:*' completer _copa_suggestion_complete ${cur:-_complete} _copa_history_complete
344
+ zstyle ':completion:*' completer _copa_suggestion_complete ${cur:-_complete} _copa_history_complete _files
344
345
  fi
345
346
  # Enable group separation so Copa results appear as a distinct section
346
347
  zstyle ':completion:*' group-name ''
@@ -349,11 +350,13 @@ _copa_history_complete() {
349
350
  # Interactive menu with highlighting; Tab accepts the focused item
350
351
  zstyle ':completion:*' menu select
351
352
  zmodload zsh/complist 2>/dev/null
352
- bindkey -M menuselect '^I' .accept-line
353
+ bindkey -M menuselect '^I' accept-search
354
+ bindkey -M menuselect '^M' accept-search
355
+ bindkey -M menuselect '^J' accept-search
353
356
  # Escape cancels the menu and restores the original buffer
354
357
  bindkey -M menuselect '^[' send-break
355
358
  # Space accepts the focused completion and closes the menu
356
- bindkey -M menuselect ' ' .accept-line
359
+ bindkey -M menuselect ' ' accept-search
357
360
  # Raise threshold before "show all N?" prompt
358
361
  LISTMAX=200
359
362
  # Copa completion branding: show group description headers
@@ -495,6 +498,20 @@ _copa_suggest_forward_word() {
495
498
  }
496
499
  zle -N forward-word _copa_suggest_forward_word
497
500
 
501
+ # end-of-line (Cmd+Right / End): accept full suggestion if ghost text showing
502
+ _copa_suggest_end_of_line() {
503
+ if [[ -n "$POSTDISPLAY" && -n "$_COPA_SUGGESTION" && $CURSOR -eq ${#BUFFER} ]]; then
504
+ BUFFER="$_COPA_SUGGESTION"
505
+ CURSOR=${#BUFFER}
506
+ _copa_suggest_clear
507
+ _COPA_SUGGEST_LATCHED=0
508
+ _copa_suggest_fetch
509
+ else
510
+ zle .end-of-line
511
+ fi
512
+ }
513
+ zle -N end-of-line _copa_suggest_end_of_line
514
+
498
515
  # accept-line (Enter): clear suggestion + latch, then execute
499
516
  _copa_suggest_accept_line() {
500
517
  _copa_suggest_clear
@@ -3,13 +3,14 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import os
6
7
  import sqlite3
7
8
  import time
8
9
  from pathlib import Path
9
10
 
10
11
  from .models import Command, SharedSet
11
12
 
12
- DEFAULT_DB_PATH = Path.home() / ".copa" / "copa.db"
13
+ DEFAULT_DB_PATH = Path(os.environ.get("COPA_DB", Path.home() / ".copa" / "copa.db"))
13
14
 
14
15
  SCHEMA_SQL = """\
15
16
  CREATE TABLE IF NOT EXISTS commands (
@@ -2,6 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
6
+ from pathlib import Path
7
+
5
8
  from .db import Database
6
9
  from .scoring import rank_commands
7
10
 
@@ -105,6 +108,49 @@ def create_mcp_server():
105
108
  db.update_description(command_id, description)
106
109
  return f"Updated [{command_id}] {cmd.command}: {description}"
107
110
 
111
+ @mcp.tool()
112
+ def copa_delete_command(command_id: int) -> str:
113
+ """Delete a command from Copa by its ID."""
114
+ cmd = db.get_command(command_id)
115
+ if not cmd:
116
+ return f"Command {command_id} not found."
117
+ db.remove_command(command_id)
118
+ return f"Deleted [{command_id}] {cmd.command}"
119
+
120
+ @mcp.tool()
121
+ def copa_set_group(command_id: int, group: str | None = None) -> str:
122
+ """Set or change the group of a command. Pass group=None to remove from group."""
123
+ cmd = db.get_command(command_id)
124
+ if not cmd:
125
+ return f"Command {command_id} not found."
126
+ db.update_group(command_id, group)
127
+ if group:
128
+ return f"Moved [{command_id}] {cmd.command} to group '{group}'"
129
+ return f"Removed [{command_id}] {cmd.command} from its group"
130
+
131
+ @mcp.tool()
132
+ def copa_update_flags(command_id: int, flags: dict[str, str]) -> str:
133
+ """Update the flags/options documentation for a command.
134
+
135
+ flags is a dict mapping flag name to description, e.g. {"-v": "Verbose output"}.
136
+ """
137
+ cmd = db.get_command(command_id)
138
+ if not cmd:
139
+ return f"Command {command_id} not found."
140
+ db.update_flags(command_id, flags)
141
+ flag_list = ", ".join(f"{k}: {v}" for k, v in flags.items())
142
+ return f"Updated flags for [{command_id}] {cmd.command}: {flag_list}"
143
+
144
+ @mcp.tool()
145
+ def copa_pin_command(command_id: int, pinned: bool = True) -> str:
146
+ """Pin or unpin a command. Pinned commands always appear at the top."""
147
+ cmd = db.get_command(command_id)
148
+ if not cmd:
149
+ return f"Command {command_id} not found."
150
+ db.pin_command(command_id, pinned)
151
+ action = "Pinned" if pinned else "Unpinned"
152
+ return f"{action} [{command_id}] {cmd.command}"
153
+
108
154
  @mcp.tool()
109
155
  def copa_create_group(name: str, commands: list[dict] | None = None) -> str:
110
156
  """Create a Copa group and optionally add commands to it.
@@ -146,6 +192,65 @@ def create_mcp_server():
146
192
  count += 1
147
193
  return f"Added {count} commands."
148
194
 
195
+ @mcp.tool()
196
+ def copa_share_load(file_path: str) -> str:
197
+ """Load a .copa file into Copa as a shared set.
198
+
199
+ The file_path should be a path to a .copa JSON file.
200
+ """
201
+ from .sharing import import_shared_set, load_copa_file, resolve_copa_path
202
+
203
+ resolved = resolve_copa_path(file_path)
204
+ if not resolved or not resolved.is_file():
205
+ return f"File not found: {file_path}"
206
+ copa_file = load_copa_file(resolved)
207
+ count = import_shared_set(db, copa_file, source_path=str(resolved))
208
+ return f"Loaded shared set '{copa_file.name}' with {count} commands from {resolved}"
209
+
210
+ @mcp.tool()
211
+ def copa_share_list() -> str:
212
+ """List all loaded shared sets."""
213
+ sets = db.get_shared_sets()
214
+ if not sets:
215
+ return "No shared sets loaded."
216
+ lines = []
217
+ for ss in sets:
218
+ parts = [f"- {ss.name}"]
219
+ if ss.description:
220
+ parts[0] += f": {ss.description}"
221
+ if ss.author:
222
+ parts.append(f" author: {ss.author}")
223
+ if ss.source_path:
224
+ parts.append(f" source: {ss.source_path}")
225
+ lines.append("\n".join(parts))
226
+ return "\n".join(lines)
227
+
228
+ @mcp.tool()
229
+ def copa_share_remove(name: str) -> str:
230
+ """Remove a shared set and its commands from Copa."""
231
+ sets = db.get_shared_sets()
232
+ if not any(ss.name == name for ss in sets):
233
+ return f"Shared set '{name}' not found."
234
+ db.remove_shared_set(name)
235
+ return f"Removed shared set '{name}'"
236
+
237
+ @mcp.tool()
238
+ def copa_export_group(group: str, output_path: str | None = None) -> str:
239
+ """Export a Copa group as a .copa JSON file.
240
+
241
+ If output_path is not provided, returns the JSON content directly.
242
+ """
243
+ from .sharing import export_group
244
+
245
+ copa_file = export_group(db, group)
246
+ if not copa_file.commands:
247
+ return f"Group '{group}' has no commands to export."
248
+ content = json.dumps(copa_file.to_dict(), indent=2)
249
+ if output_path:
250
+ Path(output_path).write_text(content)
251
+ return f"Exported group '{group}' ({len(copa_file.commands)} commands) to {output_path}"
252
+ return content
253
+
149
254
  return mcp
150
255
 
151
256
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: copa-cli
3
- Version: 0.7.1
3
+ Version: 0.8.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
@@ -36,6 +36,8 @@ Dynamic: license-file
36
36
 
37
37
  Copa tracks the commands you run, ranks them by frequency and recency, and gives you instant fuzzy search via fzf. Think of it as a smart, searchable, shareable upgrade to shell history.
38
38
 
39
+ ![Copa Setup](demos/01-setup.gif)
40
+
39
41
  ## Features
40
42
 
41
43
  - **Smart ranking** — commands scored by `2*log(1+freq) + 8*0.5^(age/3d)`, so frequent *and* recent commands float to the top
@@ -134,6 +136,8 @@ copa sync
134
136
 
135
137
  Once shell integration is sourced, pressing **Ctrl+R** opens an fzf-powered command palette instead of the default zsh reverse search. This is Copa's primary interface.
136
138
 
139
+ ![Ctrl+R Palette](demos/04-ctrl-r-palette.gif)
140
+
137
141
  ### What you see
138
142
 
139
143
  Copa pipes every tracked command into fzf with aligned columns:
@@ -173,6 +177,8 @@ While the fzf palette is open, these keys are available:
173
177
  | **Ctrl+/** | Append `2>/dev/null` | Suppress stderr |
174
178
  | **Ctrl+S** | Scope by group | Opens inline group list — Enter filters to that group, ESC returns to all |
175
179
  | **Ctrl+G** | Assign group | Opens inline group list — Enter assigns the group to the highlighted command |
180
+
181
+ ![Groups and Scoping](demos/05-groups-and-scoping.gif)
176
182
  | **Ctrl+N** | Cycle group | Cycles through groups: (all) → group1 → group2 → ... → (all) |
177
183
  | **Ctrl+D** | Describe | Generate/edit a description using LLM (with tty-aware input) |
178
184
  | **Ctrl+F** | Edit flags | Add flag documentation to the highlighted command |
@@ -184,6 +190,8 @@ Keybindings are configurable via `~/.copa/config.toml`. See [Configuration](#con
184
190
 
185
191
  ### Select mode (bulk operations)
186
192
 
193
+ ![Bulk Operations](demos/06-bulk-operations.gif)
194
+
187
195
  Press **Ctrl+B** from the Ctrl+R palette to enter **select mode**. This opens a new fzf view with multi-select enabled:
188
196
 
189
197
  - **Tab** toggles selection on individual commands
@@ -220,10 +228,14 @@ Selecting a command places it directly into your shell prompt (without executing
220
228
 
221
229
  ## Tab Completion
222
230
 
231
+ ![Tab Menu Select](demos/03-tab-menu-select.gif)
232
+
223
233
  Copa supplements zsh's built-in tab completion for **any** command — not just Copa's own CLI. Once `copa.zsh` is sourced, Copa registers as a completer in zsh's completion system.
224
234
 
225
235
  ### Completion modes
226
236
 
237
+ ![Completion Modes](demos/10-completion-modes.gif)
238
+
227
239
  Copa supports four completion modes, configured via `~/.copa/config.toml`:
228
240
 
229
241
  | Mode | Behavior |
@@ -269,6 +281,10 @@ Copa's own CLI completions (`copa li<TAB>` → `list`) continue to work as befor
269
281
 
270
282
  ## Inline Suggestions (Ghost Text)
271
283
 
284
+ | Tab Mode 1 (direct accept) | Tab Mode 2 (menu select, default) |
285
+ |---|---|
286
+ | ![Tab Mode 1](demos/02a-suggestions-tab-mode1.gif) | ![Tab Mode 2](demos/02b-suggestions-tab-mode2.gif) |
287
+
272
288
  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.
273
289
 
274
290
  ### How it works
@@ -288,6 +304,7 @@ $ git pu█sh origin main ← grey ghost text
288
304
  | **Tab** | `tab_accept=1`: accept full suggestion. `tab_accept=2` (default): open completion menu with suggestion highlighted at top, native completions below | If latched: unlatch + re-fetch suggestion. Else: normal tab completion |
289
305
  | **Down** | Open completion menu with suggestion highlighted at top | History navigation |
290
306
  | **Right arrow** | Accept one word, re-fetch | Move cursor right |
307
+ | **Cmd+Right / End** | Accept full suggestion | Move to end of line |
291
308
  | Enter | Clear suggestion, execute | Execute |
292
309
  | Esc | Dismiss suggestion | Normal Esc |
293
310
  | Up | Clear suggestion, navigate history | History navigation |
@@ -441,6 +458,8 @@ copa describe 42
441
458
 
442
459
  Generates a description for a specific command by ID. Same accept/edit flow as `fix --auto`.
443
460
 
461
+ ![Flags and Describe](demos/09-flags-and-describe.gif)
462
+
444
463
  ## Script Metadata Protocol
445
464
 
446
465
  Copa recognizes `#@` headers in script files (checked in the first 50 lines):
@@ -488,6 +507,8 @@ copa scan --dir ~/bin # scan a specific directory
488
507
 
489
508
  ## Sharing
490
509
 
510
+ ![Sharing](demos/07-sharing.gif)
511
+
491
512
  Export a group as a `.copa` file:
492
513
 
493
514
  ```bash
@@ -622,8 +643,16 @@ Available MCP tools:
622
643
  - `copa_get_stats` — usage statistics
623
644
  - `copa_add_command` — add a command
624
645
  - `copa_update_description` — update a description
646
+ - `copa_delete_command` — delete a command by ID
647
+ - `copa_set_group` — set or change a command's group
648
+ - `copa_update_flags` — update flag documentation for a command
649
+ - `copa_pin_command` — pin or unpin a command
625
650
  - `copa_create_group` — create a group with commands
626
651
  - `copa_bulk_add` — bulk add commands
652
+ - `copa_share_load` — load a .copa file as a shared set
653
+ - `copa_share_list` — list all loaded shared sets
654
+ - `copa_share_remove` — remove a shared set
655
+ - `copa_export_group` — export a group as a .copa file
627
656
 
628
657
  ## Configuration
629
658
 
@@ -671,6 +700,8 @@ continue = ["pipe", "chain", "redirect"] # default: |, &&, > re-open fzf
671
700
 
672
701
  ### Composition key behavior
673
702
 
703
+ ![Composition](demos/08-composition.gif)
704
+
674
705
  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).
675
706
 
676
707
  By default, "connector" operators re-open fzf:
@@ -692,6 +723,10 @@ To revert to the old behavior (all keys close immediately), set:
692
723
  continue = []
693
724
  ```
694
725
 
726
+ ## End-to-End Workflow
727
+
728
+ ![Workflow](demos/11-workflow.gif)
729
+
695
730
  ## CLI Reference
696
731
 
697
732
  | Command | Purpose |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "copa-cli"
7
- version = "0.7.1"
7
+ version = "0.8.0"
8
8
  description = "Command Palette — smart command tracking, ranking, and sharing for your shell"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1046,3 +1046,39 @@ class TestTabAcceptZsh:
1046
1046
  content = self._read_zsh("copa.zsh")
1047
1047
  assert "copa-suggestion" in content
1048
1048
  assert "-X 'SUGGESTED'" in content
1049
+
1050
+ def test_packaged_zsh_menuselect_uses_accept_search(self):
1051
+ content = self._read_zsh("copa/copa.zsh")
1052
+ assert "bindkey -M menuselect '^I' accept-search" in content
1053
+ assert "bindkey -M menuselect '^M' accept-search" in content
1054
+ assert ".accept-line" not in content or "zle .accept-line" in content
1055
+
1056
+ def test_root_zsh_menuselect_uses_accept_search(self):
1057
+ content = self._read_zsh("copa.zsh")
1058
+ assert "bindkey -M menuselect '^I' accept-search" in content
1059
+ assert "bindkey -M menuselect '^M' accept-search" in content
1060
+ assert ".accept-line" not in content or "zle .accept-line" in content
1061
+
1062
+ def test_packaged_zsh_end_of_line_accepts_full(self):
1063
+ content = self._read_zsh("copa/copa.zsh")
1064
+ start = content.index("_copa_suggest_end_of_line()")
1065
+ func_block = content[start : start + 400]
1066
+ assert "_COPA_SUGGESTION" in func_block
1067
+ assert 'BUFFER="$_COPA_SUGGESTION"' in func_block
1068
+ assert "zle .end-of-line" in func_block
1069
+
1070
+ def test_root_zsh_end_of_line_accepts_full(self):
1071
+ content = self._read_zsh("copa.zsh")
1072
+ start = content.index("_copa_suggest_end_of_line()")
1073
+ func_block = content[start : start + 400]
1074
+ assert "_COPA_SUGGESTION" in func_block
1075
+ assert 'BUFFER="$_COPA_SUGGESTION"' in func_block
1076
+ assert "zle .end-of-line" in func_block
1077
+
1078
+ def test_packaged_zsh_end_of_line_registered(self):
1079
+ content = self._read_zsh("copa/copa.zsh")
1080
+ assert "zle -N end-of-line _copa_suggest_end_of_line" in content
1081
+
1082
+ def test_root_zsh_end_of_line_registered(self):
1083
+ content = self._read_zsh("copa.zsh")
1084
+ assert "zle -N end-of-line _copa_suggest_end_of_line" in content
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