ccmd 2.0.2__tar.gz → 2.0.4__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 (54) hide show
  1. {ccmd-2.0.2/ccmd.egg-info → ccmd-2.0.4}/PKG-INFO +1 -1
  2. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/__init__.py +1 -1
  3. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/screens/browser.py +9 -7
  4. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/screens/hub.py +59 -31
  5. {ccmd-2.0.2 → ccmd-2.0.4/ccmd.egg-info}/PKG-INFO +1 -1
  6. {ccmd-2.0.2 → ccmd-2.0.4}/pyproject.toml +1 -1
  7. {ccmd-2.0.2 → ccmd-2.0.4}/LICENSE +0 -0
  8. {ccmd-2.0.2 → ccmd-2.0.4}/MANIFEST.in +0 -0
  9. {ccmd-2.0.2 → ccmd-2.0.4}/README.md +0 -0
  10. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/__main__.py +0 -0
  11. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/cli.py +0 -0
  12. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/config/__init__.py +0 -0
  13. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/config/paths.py +0 -0
  14. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/config/schema.py +0 -0
  15. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/__init__.py +0 -0
  16. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/agents/__init__.py +0 -0
  17. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/agents/aider.py +0 -0
  18. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/agents/base.py +0 -0
  19. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/agents/chatgpt.py +0 -0
  20. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/agents/claude.py +0 -0
  21. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/agents/codex.py +0 -0
  22. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/agents/cursor.py +0 -0
  23. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/agents/goose.py +0 -0
  24. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/agents/grok.py +0 -0
  25. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/fs.py +0 -0
  26. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/launcher.py +0 -0
  27. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/migrate.py +0 -0
  28. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/profiles.py +0 -0
  29. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/projects.py +0 -0
  30. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/scan.py +0 -0
  31. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/sessions.py +0 -0
  32. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/core/ssh.py +0 -0
  33. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/mcp/__init__.py +0 -0
  34. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/mcp/server.py +0 -0
  35. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/__init__.py +0 -0
  36. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/app.py +0 -0
  37. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/ascii.py +0 -0
  38. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/screens/__init__.py +0 -0
  39. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/screens/agents.py +0 -0
  40. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/screens/aliases.py +0 -0
  41. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/screens/project_edit.py +0 -0
  42. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/screens/sessions.py +0 -0
  43. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/screens/ssh.py +0 -0
  44. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/theme.py +0 -0
  45. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/widgets/__init__.py +0 -0
  46. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd/tui/widgets/spinner.py +0 -0
  47. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd.egg-info/SOURCES.txt +0 -0
  48. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd.egg-info/dependency_links.txt +0 -0
  49. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd.egg-info/entry_points.txt +0 -0
  50. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd.egg-info/requires.txt +0 -0
  51. {ccmd-2.0.2 → ccmd-2.0.4}/ccmd.egg-info/top_level.txt +0 -0
  52. {ccmd-2.0.2 → ccmd-2.0.4}/commands.yaml +0 -0
  53. {ccmd-2.0.2 → ccmd-2.0.4}/requirements.txt +0 -0
  54. {ccmd-2.0.2 → ccmd-2.0.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccmd
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: ccmd — Agentic Project Hub. Pick a project, pick an agent, launch.
5
5
  Author-email: De Catalyst <Robert5560newton@gmail.com>
6
6
  License: MIT
@@ -1,4 +1,4 @@
1
1
  """ccmd 2.0 — Agentic Project Hub."""
2
2
 
3
- __version__ = "2.0.2"
3
+ __version__ = "2.0.4"
4
4
  __all__ = ["__version__"]
@@ -12,6 +12,8 @@ from textual.screen import Screen
12
12
  from textual.widgets import Footer, Input, OptionList, Static
13
13
  from textual.widgets.option_list import Option
14
14
 
15
+ from rich.markup import escape
16
+
15
17
  from ccmd.config.schema import load_config
16
18
  from ccmd.core.agents import all_agents, get_agent
17
19
  from ccmd.core.fs import default_browse_roots, is_projectish, list_dir_entries
@@ -98,9 +100,9 @@ class DirBrowserScreen(Screen[bool | None]):
98
100
  ag = get_agent(self._agent)
99
101
  ag_s = f"{ag.ascii_logo} {self._agent}" if ag else self._agent
100
102
  bar.update(
101
- f"[bold #00ffc8]{self._cwd}[/]{mark}{hidden} · "
102
- f"agent [bold #c84bff]{ag_s}[/]"
103
- + (f" [#6a6a80]{extra}[/]" if extra else "")
103
+ f"[bold #00ffc8]{escape(str(self._cwd))}[/]{mark}{hidden} · "
104
+ f"agent [bold #c84bff]{escape(ag_s)}[/]"
105
+ + (f" [#6a6a80]{escape(extra)}[/]" if extra else "")
104
106
  )
105
107
 
106
108
  def _render_list(self) -> None:
@@ -184,21 +186,21 @@ class DirBrowserScreen(Screen[bool | None]):
184
186
  kids = list_dir_entries(resolved, show_hidden=self._show_hidden, dirs_only=True)
185
187
  project = is_projectish(resolved)
186
188
  lines = [
187
- f"[bold #00ffc8]{resolved.name or resolved}[/]",
188
- f"path {resolved}",
189
+ f"[bold #00ffc8]{escape(str(resolved.name or resolved))}[/]",
190
+ f"path {escape(str(resolved))}",
189
191
  f"project {'yes ◆' if project else 'no (still selectable)'}",
190
192
  f"subdirs {len(kids)}",
191
193
  "",
192
194
  "[#6a6a80]children[/]",
193
195
  ]
194
196
  for name, _, _ in kids[:12]:
195
- lines.append(f" · {name}/")
197
+ lines.append(f" · {escape(name)}/")
196
198
  if len(kids) > 12:
197
199
  lines.append(f" … +{len(kids) - 12} more")
198
200
  lines.extend(
199
201
  [
200
202
  "",
201
- "[#6a6a80] open folder · s select as project[/]",
203
+ "[#6a6a80]enter open folder · s select as project[/]",
202
204
  ]
203
205
  )
204
206
  prev.update("\n".join(lines))
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from pathlib import Path
6
6
 
7
+ from rich.markup import escape
7
8
  from textual import on, work
8
9
  from textual.app import ComposeResult
9
10
  from textual.binding import Binding
@@ -33,6 +34,7 @@ def _agent_bar() -> str:
33
34
 
34
35
 
35
36
  class HubScreen(Screen):
37
+ # Filter Input is can_focus=False until / is pressed, so f/b/a never type into it.
36
38
  BINDINGS = [
37
39
  Binding("enter", "open_project", "Open", show=True),
38
40
  Binding("a", "pick_agent", "Agents", show=True),
@@ -44,13 +46,13 @@ class HubScreen(Screen):
44
46
  Binding("h", "ssh", "SSH", show=True),
45
47
  Binding("l", "aliases", "Aliases", show=True),
46
48
  Binding("d", "detach", "Detach", show=True),
49
+ Binding("r", "refresh", "Refresh", show=True),
50
+ Binding("q", "quit", "Quit", show=True),
47
51
  Binding("left_square_bracket", "agent_prev", "Agent−", show=True),
48
52
  Binding("right_square_bracket", "agent_next", "Agent+", show=True),
49
53
  Binding("slash", "focus_filter", "Filter", show=False),
50
54
  Binding("/", "focus_filter", "Filter", show=True),
51
- Binding("r", "refresh", "Refresh", show=True),
52
- Binding("q", "quit", "Quit", show=True),
53
- # 1–7 set default agent
55
+ Binding("escape", "blur_filter", "Esc", show=False),
54
56
  Binding("1", "agent_n_1", show=False),
55
57
  Binding("2", "agent_n_2", show=False),
56
58
  Binding("3", "agent_n_3", show=False),
@@ -74,7 +76,11 @@ class HubScreen(Screen):
74
76
  id="subtitle",
75
77
  )
76
78
  yield Static(_agent_bar(), id="agent-bar")
77
- yield Input(placeholder="filter projects… (/ to focus)", id="filter")
79
+ # can_focus stays False until / prevents f/b/a landing in the box
80
+ yield Input(
81
+ placeholder="press / to filter projects…",
82
+ id="filter",
83
+ )
78
84
  with Horizontal(id="main"):
79
85
  with Vertical(id="projects-panel"):
80
86
  yield Static("projects (registry — not whole disk)", classes="panel-title")
@@ -84,13 +90,14 @@ class HubScreen(Screen):
84
90
  yield Static("Select a project", id="detail")
85
91
  yield Static("", id="status")
86
92
  yield Static(
87
- "↵ open · a launch w/ agent · A set default · ]/[ cycle · 1-7 agent · "
88
- "b browse · f /mnt · d detach · q quit",
93
+ "↵ open · f /mnt · b browse · / filter · esc list · a agent · ] cycle · q quit",
89
94
  id="footer-bar",
90
95
  )
91
96
  yield Footer()
92
97
 
93
98
  def on_mount(self) -> None:
99
+ filt = self.query_one("#filter", Input)
100
+ filt.can_focus = False
94
101
  self._load_projects()
95
102
  self.query_one("#project-list", OptionList).focus()
96
103
  if not self._projects:
@@ -101,10 +108,9 @@ class HubScreen(Screen):
101
108
  )
102
109
  self.set_timer(0.3, self.action_browse)
103
110
  else:
104
- # hint once if everything is claude
105
111
  if all(p.default_agent == "claude" for p in self._projects[:20]):
106
112
  self.notify(
107
- "tip: ] cycles default agent · 1–7 picks agent · f browses /mnt",
113
+ "tip: f = browse /mnt · / = filter · ] = cycle agent",
108
114
  severity="information",
109
115
  timeout=5,
110
116
  )
@@ -171,7 +177,7 @@ class HubScreen(Screen):
171
177
  "[#6a6a80]This list is your saved registry.\n"
172
178
  "Press [bold]f[/] to browse /mnt and subdirs,\n"
173
179
  "or [bold]b[/] for home + all roots.\n"
174
- "[bold]][/] cycles default agent on selection.[/]"
180
+ "Press ] to cycle default agent.[/]"
175
181
  )
176
182
  return
177
183
  agent = get_agent(p.default_agent)
@@ -181,29 +187,32 @@ class HubScreen(Screen):
181
187
  else p.default_agent
182
188
  )
183
189
  exists = "yes" if p.exists() else "MISSING"
184
- # agent roster
185
190
  roster: list[str] = []
186
191
  for i, a in enumerate(self._agent_info, start=1):
187
192
  mark = "●" if a["id"] == p.default_agent else "·"
188
193
  ready = "✓" if a["installed"] else "✗"
189
- roster.append(f" {mark} {i} {a['logo']} {a['id']:<8} {ready}")
194
+ roster.append(
195
+ f" {mark} {i} {escape(str(a['logo']))} "
196
+ f"{escape(a['id']):<8} {ready}"
197
+ )
190
198
 
199
+ # Escape anything that can contain [ ] so Rich markup doesn't explode
191
200
  lines = [
192
- f"[bold #00ffc8]{p.name}[/]",
193
- f"id {p.id}",
194
- f"path {p.path}",
195
- f"root {_root_label(p.path)}",
201
+ f"[bold #00ffc8]{escape(p.name)}[/]",
202
+ f"id {escape(p.id)}",
203
+ f"path {escape(p.path)}",
204
+ f"root {escape(_root_label(p.path))}",
196
205
  f"exists {exists}",
197
- f"agent [bold]{agent_line}[/] (default)",
198
- f"ssh {p.ssh_host or '—'}",
199
- f"env {', '.join(p.env_files) or '—'}",
200
- f"tags {', '.join(p.tags) or '—'}",
201
- f"opened {p.last_opened_at or 'never'}",
206
+ f"agent [bold]{escape(agent_line)}[/] (default)",
207
+ f"ssh {escape(p.ssh_host or '—')}",
208
+ f"env {escape(', '.join(p.env_files) or '—')}",
209
+ f"tags {escape(', '.join(p.tags) or '—')}",
210
+ f"opened {escape(p.last_opened_at or 'never')}",
202
211
  "",
203
- "[bold #c84bff]agents[/] ]/[ cycle · 1-7 set · A set only · a launch",
212
+ "[bold #c84bff]agents[/] keys: ] [ cycle · 1-7 set · A set · a launch",
204
213
  *roster,
205
214
  "",
206
- "[#6a6a80] open · d detach · b/f add from disk[/]",
215
+ "[#6a6a80]enter open · d detach · b/f add from disk[/]",
207
216
  ]
208
217
  detail.update("\n".join(lines))
209
218
 
@@ -255,7 +264,23 @@ class HubScreen(Screen):
255
264
  # ── actions ──────────────────────────────────────────────
256
265
 
257
266
  def action_focus_filter(self) -> None:
258
- self.query_one("#filter", Input).focus()
267
+ """Only way into the filter — so f never types into the box by accident."""
268
+ filt = self.query_one("#filter", Input)
269
+ filt.can_focus = True
270
+ filt.focus()
271
+
272
+ def action_blur_filter(self) -> None:
273
+ """Esc: leave filter, give keys back to hub (f/b/a work again)."""
274
+ filt = self.query_one("#filter", Input)
275
+ if self.focused is filt or filt.has_focus:
276
+ filt.can_focus = False
277
+ self.query_one("#project-list", OptionList).focus()
278
+ return
279
+ # if not in filter, esc clears filter text
280
+ if filt.value:
281
+ filt.value = ""
282
+ self._load_projects("")
283
+ self.query_one("#project-list", OptionList).focus()
259
284
 
260
285
  def action_refresh(self) -> None:
261
286
  self._agent_info = detect_agents()
@@ -434,19 +459,22 @@ class HubScreen(Screen):
434
459
 
435
460
 
436
461
  def _root_label(path: str) -> str:
437
- """Short badge for where the project lives."""
462
+ """Short badge for where the project lives.
463
+
464
+ Uses angle brackets, not square ones — square brackets break Rich markup.
465
+ """
438
466
  p = path.replace("\\", "/")
439
467
  if p.startswith("/mnt/vylth"):
440
- return "[vylth]"
468
+ return "<vylth>"
441
469
  if p.startswith("/mnt/c"):
442
- return "[win-c]"
470
+ return "<win-c>"
443
471
  if p.startswith("/mnt/d"):
444
- return "[win-d]"
472
+ return "<win-d>"
445
473
  if p.startswith("/mnt/"):
446
474
  part = p.split("/")[2] if len(p.split("/")) > 2 else "mnt"
447
- return f"[mnt/{part}]"
475
+ return f"<mnt/{part}>"
448
476
  if p.startswith(str(Path.home())):
449
- return "[home]"
477
+ return "<home>"
450
478
  if p.startswith("/opt"):
451
- return "[opt]"
452
- return "[disk]"
479
+ return "<opt>"
480
+ return "<disk>"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccmd
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: ccmd — Agentic Project Hub. Pick a project, pick an agent, launch.
5
5
  Author-email: De Catalyst <Robert5560newton@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ccmd"
7
- version = "2.0.2"
7
+ version = "2.0.4"
8
8
  description = "ccmd — Agentic Project Hub. Pick a project, pick an agent, launch."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
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
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