ccmd 2.0.5__tar.gz → 2.0.7__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 (55) hide show
  1. {ccmd-2.0.5/ccmd.egg-info → ccmd-2.0.7}/PKG-INFO +1 -1
  2. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/__init__.py +1 -1
  3. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/cli.py +62 -0
  4. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/projects.py +17 -2
  5. ccmd-2.0.7/ccmd/core/shell_cmds.py +352 -0
  6. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/screens/hub.py +4 -0
  7. {ccmd-2.0.5 → ccmd-2.0.7/ccmd.egg-info}/PKG-INFO +1 -1
  8. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd.egg-info/SOURCES.txt +1 -0
  9. {ccmd-2.0.5 → ccmd-2.0.7}/pyproject.toml +1 -1
  10. {ccmd-2.0.5 → ccmd-2.0.7}/LICENSE +0 -0
  11. {ccmd-2.0.5 → ccmd-2.0.7}/MANIFEST.in +0 -0
  12. {ccmd-2.0.5 → ccmd-2.0.7}/README.md +0 -0
  13. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/__main__.py +0 -0
  14. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/config/__init__.py +0 -0
  15. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/config/paths.py +0 -0
  16. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/config/schema.py +0 -0
  17. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/__init__.py +0 -0
  18. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/agents/__init__.py +0 -0
  19. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/agents/aider.py +0 -0
  20. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/agents/base.py +0 -0
  21. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/agents/chatgpt.py +0 -0
  22. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/agents/claude.py +0 -0
  23. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/agents/codex.py +0 -0
  24. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/agents/cursor.py +0 -0
  25. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/agents/goose.py +0 -0
  26. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/agents/grok.py +0 -0
  27. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/fs.py +0 -0
  28. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/launcher.py +0 -0
  29. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/migrate.py +0 -0
  30. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/profiles.py +0 -0
  31. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/scan.py +0 -0
  32. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/sessions.py +0 -0
  33. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/core/ssh.py +0 -0
  34. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/mcp/__init__.py +0 -0
  35. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/mcp/server.py +0 -0
  36. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/__init__.py +0 -0
  37. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/app.py +0 -0
  38. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/ascii.py +0 -0
  39. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/screens/__init__.py +0 -0
  40. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/screens/agents.py +0 -0
  41. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/screens/aliases.py +0 -0
  42. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/screens/browser.py +0 -0
  43. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/screens/project_edit.py +0 -0
  44. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/screens/sessions.py +0 -0
  45. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/screens/ssh.py +0 -0
  46. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/theme.py +0 -0
  47. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/widgets/__init__.py +0 -0
  48. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd/tui/widgets/spinner.py +0 -0
  49. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd.egg-info/dependency_links.txt +0 -0
  50. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd.egg-info/entry_points.txt +0 -0
  51. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd.egg-info/requires.txt +0 -0
  52. {ccmd-2.0.5 → ccmd-2.0.7}/ccmd.egg-info/top_level.txt +0 -0
  53. {ccmd-2.0.5 → ccmd-2.0.7}/commands.yaml +0 -0
  54. {ccmd-2.0.5 → ccmd-2.0.7}/requirements.txt +0 -0
  55. {ccmd-2.0.5 → ccmd-2.0.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccmd
3
- Version: 2.0.5
3
+ Version: 2.0.7
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.5"
3
+ __version__ = "2.0.7"
4
4
  __all__ = ["__version__"]
@@ -20,6 +20,55 @@ from ccmd.core.sessions import SessionStore
20
20
  from ccmd.core.ssh import SSHStore
21
21
 
22
22
 
23
+ def _cmd_shell(args: argparse.Namespace) -> int:
24
+ from ccmd.core.shell_cmds import (
25
+ install_shell_hook,
26
+ list_cmd_rows,
27
+ shell_cmds_path,
28
+ write_shell_cmds,
29
+ )
30
+
31
+ sub = args.shell_cmd or "regen"
32
+ if sub == "install":
33
+ path = write_shell_cmds()
34
+ rcs = install_shell_hook()
35
+ print(f"wrote {path}")
36
+ for rc in rcs:
37
+ print(f"hooked {rc}")
38
+ print("restart shell or: source ~/.ccmd/shell_cmds.sh")
39
+ print("then: ana / anac / anag …")
40
+ return 0
41
+ if sub == "regen":
42
+ path = write_shell_cmds()
43
+ print(f"regenerated {path}")
44
+ rows = list_cmd_rows()
45
+ for r in rows[:30]:
46
+ print(
47
+ f" {r['short']:<10} {r['project']:<18} "
48
+ f"default={r['default_agent']:<8} "
49
+ f"{r['short']}c/g/x/u/o/a/p"
50
+ )
51
+ if len(rows) > 30:
52
+ print(f" … +{len(rows) - 30} more (see {path})")
53
+ return 0
54
+ if sub == "list":
55
+ for r in list_cmd_rows():
56
+ vars_ = " ".join(
57
+ f"{k[0]}={v}" for k, v in r["variants"].items()
58
+ )
59
+ # show compact
60
+ v = r["variants"]
61
+ print(
62
+ f"{r['short']:<10} {r['project']:<20} "
63
+ f"{v.get('claude','')} {v.get('grok','')} {v.get('codex','')} "
64
+ f"{v.get('cursor','')} → {r['path']}"
65
+ )
66
+ print(f"\nfile: {shell_cmds_path()}")
67
+ return 0
68
+ print("usage: ccmd shell install|regen|list", file=sys.stderr)
69
+ return 1
70
+
71
+
23
72
  def _cmd_open(args: argparse.Namespace) -> int:
24
73
  reg = ProjectRegistry()
25
74
  project = reg.get(args.project)
@@ -245,6 +294,18 @@ def build_parser() -> argparse.ArgumentParser:
245
294
  sub.add_parser("mcp", help="Run MCP stdio server")
246
295
  sub.add_parser("tui", help="Force open TUI")
247
296
 
297
+ sh = sub.add_parser(
298
+ "shell",
299
+ help="Short project commands (ana/anac/anag…) for bash/zsh",
300
+ )
301
+ sh.add_argument(
302
+ "shell_cmd",
303
+ nargs="?",
304
+ choices=["install", "regen", "list"],
305
+ default="regen",
306
+ help="install=write cmds + hook bashrc; regen=rewrite cmds; list=show",
307
+ )
308
+
248
309
  return p
249
310
 
250
311
 
@@ -274,6 +335,7 @@ def main(argv: list[str] | None = None) -> int:
274
335
  "aliases": _cmd_aliases,
275
336
  "doctor": _cmd_doctor,
276
337
  "mcp": _cmd_mcp,
338
+ "shell": _cmd_shell,
277
339
  }
278
340
  handler = handlers.get(args.cmd)
279
341
  if not handler:
@@ -30,6 +30,7 @@ class Project:
30
30
  name: str
31
31
  path: str
32
32
  default_agent: str = "claude"
33
+ short: str = "" # shell shortcut base, e.g. "ana" → ana/anac/anag…
33
34
  tags: list[str] = field(default_factory=list)
34
35
  env_files: list[str] = field(default_factory=lambda: [".env"])
35
36
  path_prepend: list[str] = field(default_factory=list)
@@ -60,6 +61,7 @@ class Project:
60
61
  name=str(data.get("name") or data["id"]),
61
62
  path=str(data["path"]),
62
63
  default_agent=str(data.get("default_agent") or "claude"),
64
+ short=str(data.get("short") or ""),
63
65
  tags=list(data.get("tags") or []),
64
66
  env_files=list(data.get("env_files") or [".env"]),
65
67
  path_prepend=list(data.get("path_prepend") or []),
@@ -103,7 +105,7 @@ class ProjectRegistry:
103
105
  return None
104
106
  return Project.from_dict(data)
105
107
 
106
- def save(self, project: Project) -> Project:
108
+ def save(self, project: Project, *, regen_shell: bool = True) -> Project:
107
109
  project.updated_at = _now()
108
110
  path = self._file(project.id)
109
111
  path.write_text(
@@ -111,12 +113,25 @@ class ProjectRegistry:
111
113
  encoding="utf-8",
112
114
  )
113
115
  path.chmod(0o600)
116
+ if regen_shell:
117
+ try:
118
+ from ccmd.core.shell_cmds import write_shell_cmds
119
+
120
+ write_shell_cmds()
121
+ except Exception:
122
+ pass
114
123
  return project
115
124
 
116
125
  def delete(self, project_id: str) -> bool:
117
126
  path = self._file(project_id)
118
127
  if path.exists():
119
128
  path.unlink()
129
+ try:
130
+ from ccmd.core.shell_cmds import write_shell_cmds
131
+
132
+ write_shell_cmds()
133
+ except Exception:
134
+ pass
120
135
  return True
121
136
  return False
122
137
 
@@ -150,7 +165,7 @@ class ProjectRegistry:
150
165
  if not p:
151
166
  return
152
167
  p.last_opened_at = _now()
153
- self.save(p)
168
+ self.save(p, regen_shell=False)
154
169
 
155
170
  def fuzzy(self, query: str, limit: int = 50) -> list[Project]:
156
171
  projects = self.list()
@@ -0,0 +1,352 @@
1
+ """Generate per-project shell shortcuts (ana, anac, anag, …).
2
+
3
+ When a project is registered, users get short commands:
4
+
5
+ ana → ccmd open anastasis (default agent)
6
+ anac → ccmd open anastasis --agent claude
7
+ anag → grok
8
+ anax → codex
9
+ anau → cursor
10
+ anao → goose
11
+ anaa → aider
12
+ anap → chatgpt
13
+
14
+ Functions live in ~/.ccmd/shell_cmds.sh and are sourced from a managed
15
+ block in ~/.bashrc / ~/.zshrc. They call `ccmd open` (no eval-cd).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ import shutil
22
+ from pathlib import Path
23
+
24
+ from ccmd.config.paths import ensure_dirs, generated_dir, home
25
+
26
+ # Single-letter agent suffixes (c = claude, so cursor uses u)
27
+ AGENT_SUFFIX: dict[str, str] = {
28
+ "claude": "c",
29
+ "grok": "g",
30
+ "codex": "x",
31
+ "cursor": "u",
32
+ "goose": "o",
33
+ "aider": "a",
34
+ "chatgpt": "p",
35
+ }
36
+
37
+ # Names we must never generate (shell builtins / common tools)
38
+ RESERVED = frozenset(
39
+ {
40
+ "cd",
41
+ "ls",
42
+ "ll",
43
+ "la",
44
+ "pwd",
45
+ "rm",
46
+ "cp",
47
+ "mv",
48
+ "cat",
49
+ "less",
50
+ "more",
51
+ "head",
52
+ "tail",
53
+ "grep",
54
+ "find",
55
+ "which",
56
+ "type",
57
+ "echo",
58
+ "printf",
59
+ "test",
60
+ "true",
61
+ "false",
62
+ "exit",
63
+ "export",
64
+ "source",
65
+ "alias",
66
+ "unalias",
67
+ "bg",
68
+ "fg",
69
+ "jobs",
70
+ "kill",
71
+ "ps",
72
+ "top",
73
+ "htop",
74
+ "ssh",
75
+ "scp",
76
+ "git",
77
+ "vim",
78
+ "nvim",
79
+ "nano",
80
+ "code",
81
+ "cursor",
82
+ "claude",
83
+ "grok",
84
+ "codex",
85
+ "go",
86
+ "node",
87
+ "npm",
88
+ "npx",
89
+ "pip",
90
+ "python",
91
+ "python3",
92
+ "ccmd",
93
+ "sudo",
94
+ "su",
95
+ "man",
96
+ "help",
97
+ "history",
98
+ "clear",
99
+ "reset",
100
+ "time",
101
+ "date",
102
+ "whoami",
103
+ "env",
104
+ "set",
105
+ "unset",
106
+ "pushd",
107
+ "popd",
108
+ "dirs",
109
+ "tmux",
110
+ "screen",
111
+ "docker",
112
+ "make",
113
+ "curl",
114
+ "wget",
115
+ "tar",
116
+ "zip",
117
+ "unzip",
118
+ }
119
+ )
120
+
121
+ MARKER_BEGIN = "# >>> ccmd shell cmds >>>"
122
+ MARKER_END = "# <<< ccmd shell cmds <<<"
123
+
124
+
125
+ def shell_cmds_path() -> Path:
126
+ ensure_dirs()
127
+ return home() / "shell_cmds.sh"
128
+
129
+
130
+ def short_candidates(project_id: str, name: str) -> list[str]:
131
+ """Generate preferred short-name candidates for a project.
132
+
133
+ Prefer snappy 3–4 letter names (ana, flo) before the full slug.
134
+ """
135
+ raw = re.sub(r"[^a-z0-9]+", "", (project_id or name).lower())
136
+ if not raw:
137
+ raw = "proj"
138
+ cands: list[str] = []
139
+ # progressive prefixes first: ana, anas, anast… (user wants ana not anastasis)
140
+ for n in (3, 4, 5, 6, 7, 8):
141
+ if n <= len(raw):
142
+ cands.append(raw[:n])
143
+ # first letters of hyphen/underscore parts (build-your-own → byo)
144
+ parts = re.split(r"[-_\s]+", project_id or name)
145
+ initials = "".join(p[0] for p in parts if p)[:6].lower()
146
+ initials = re.sub(r"[^a-z0-9]", "", initials)
147
+ if len(initials) >= 2:
148
+ cands.append(initials)
149
+ # consonants-only compact form
150
+ cons = re.sub(r"[aeiou]", "", raw)
151
+ if len(cons) >= 3:
152
+ cands.append(cons[:4])
153
+ # full slug last (only if reasonably short)
154
+ if 2 <= len(raw) <= 10:
155
+ cands.append(raw)
156
+ # dedupe preserve order
157
+ seen: set[str] = set()
158
+ out: list[str] = []
159
+ for c in cands:
160
+ if c and c not in seen and c[0].isalpha():
161
+ seen.add(c)
162
+ out.append(c)
163
+ return out or [f"p{raw[:6]}"]
164
+
165
+
166
+ def allocate_shorts(projects: list) -> dict[str, str]:
167
+ """Map project.id → unique short name."""
168
+ used: set[str] = set(RESERVED)
169
+ # honor existing project.short if set and unique
170
+ mapping: dict[str, str] = {}
171
+ for p in projects:
172
+ existing = getattr(p, "short", None) or ""
173
+ existing = re.sub(r"[^a-z0-9]", "", existing.lower())
174
+ if existing and existing not in used and existing[0].isalpha():
175
+ mapping[p.id] = existing
176
+ used.add(existing)
177
+ # also reserve agent variants
178
+ for suf in AGENT_SUFFIX.values():
179
+ used.add(existing + suf)
180
+
181
+ for p in projects:
182
+ if p.id in mapping:
183
+ continue
184
+ chosen = None
185
+ for cand in short_candidates(p.id, p.name):
186
+ if cand in used:
187
+ continue
188
+ # agent variants must also be free
189
+ if any((cand + s) in used for s in AGENT_SUFFIX.values()):
190
+ continue
191
+ chosen = cand
192
+ break
193
+ if not chosen:
194
+ # fallback numbered
195
+ base = short_candidates(p.id, p.name)[0][:4]
196
+ n = 2
197
+ while True:
198
+ cand = f"{base}{n}"
199
+ if cand not in used and all(
200
+ (cand + s) not in used for s in AGENT_SUFFIX.values()
201
+ ):
202
+ chosen = cand
203
+ break
204
+ n += 1
205
+ mapping[p.id] = chosen
206
+ used.add(chosen)
207
+ for suf in AGENT_SUFFIX.values():
208
+ used.add(chosen + suf)
209
+ return mapping
210
+
211
+
212
+ def render_shell_cmds(projects: list, shorts: dict[str, str]) -> str:
213
+ """Bash/zsh-compatible function definitions."""
214
+ lines = [
215
+ "#!/usr/bin/env bash",
216
+ "# Auto-generated by ccmd — do not edit by hand.",
217
+ "# Regenerate: ccmd shell regen",
218
+ "#",
219
+ "# Usage examples:",
220
+ "# ana open project with its default agent",
221
+ "# anac same project + claude",
222
+ "# anag + grok anax + codex anau + cursor",
223
+ "# anao + goose anaa + aider anap + chatgpt",
224
+ "#",
225
+ "",
226
+ ]
227
+ # stable order by short name
228
+ rows = sorted(
229
+ ((shorts[p.id], p) for p in projects if p.id in shorts),
230
+ key=lambda t: t[0],
231
+ )
232
+ for short, p in rows:
233
+ pid = p.id
234
+ agent = p.default_agent or "claude"
235
+ lines.append(f"# {p.name} → {p.path} (default: {agent})")
236
+ lines.append(
237
+ f"{short}() {{ command ccmd open {pid} \"$@\"; }}"
238
+ )
239
+ for agent_id, suf in AGENT_SUFFIX.items():
240
+ fn = f"{short}{suf}"
241
+ lines.append(
242
+ f"{fn}() {{ command ccmd open {pid} --agent {agent_id} \"$@\"; }}"
243
+ )
244
+ lines.append("")
245
+
246
+ lines.append("# end ccmd shell cmds")
247
+ lines.append("")
248
+ return "\n".join(lines)
249
+
250
+
251
+ def write_shell_cmds(projects: list | None = None) -> Path:
252
+ """Write ~/.ccmd/shell_cmds.sh and persist short names on projects."""
253
+ from ccmd.core.projects import ProjectRegistry
254
+
255
+ reg = ProjectRegistry()
256
+ projects = list(projects) if projects is not None else reg.list()
257
+ shorts = allocate_shorts(projects)
258
+
259
+ # persist short onto each project yaml
260
+ for p in projects:
261
+ short = shorts.get(p.id)
262
+ if short and getattr(p, "short", None) != short:
263
+ p.short = short
264
+ reg.save(p, regen_shell=False)
265
+
266
+ path = shell_cmds_path()
267
+ path.write_text(render_shell_cmds(projects, shorts), encoding="utf-8")
268
+ path.chmod(0o644)
269
+
270
+ # also drop a cheat-sheet
271
+ sheet = generated_dir() / "cmds.txt"
272
+ lines = ["# ccmd short commands\n"]
273
+ for p in sorted(projects, key=lambda x: shorts.get(x.id, x.id)):
274
+ s = shorts.get(p.id, "?")
275
+ lines.append(
276
+ f"{s:<10} {p.id:<20} default={p.default_agent:<8} "
277
+ f"+c claude +g grok +x codex +u cursor +o goose +a aider +p chatgpt\n"
278
+ )
279
+ lines.append(f"{'':10} {p.path}\n")
280
+ sheet.write_text("".join(lines), encoding="utf-8")
281
+ return path
282
+
283
+
284
+ def install_shell_hook(rc_file: Path | None = None) -> list[Path]:
285
+ """Ensure bashrc/zshrc sources shell_cmds.sh (managed block)."""
286
+ ensure_dirs()
287
+ write_shell_cmds()
288
+ block = (
289
+ f"\n{MARKER_BEGIN}\n"
290
+ f"# managed by ccmd — short project commands (ana/anac/anag…)\n"
291
+ f'[ -f "$HOME/.ccmd/shell_cmds.sh" ] && . "$HOME/.ccmd/shell_cmds.sh"\n'
292
+ f"{MARKER_END}\n"
293
+ )
294
+ updated: list[Path] = []
295
+ candidates = []
296
+ if rc_file:
297
+ candidates = [rc_file]
298
+ else:
299
+ home_dir = Path.home()
300
+ for name in (".bashrc", ".zshrc", ".bash_profile"):
301
+ p = home_dir / name
302
+ if p.exists() or name == ".bashrc":
303
+ candidates.append(p)
304
+
305
+ for rc in candidates:
306
+ try:
307
+ text = rc.read_text(encoding="utf-8") if rc.exists() else ""
308
+ except OSError:
309
+ continue
310
+ if MARKER_BEGIN in text:
311
+ # replace existing block
312
+ pattern = re.compile(
313
+ re.escape(MARKER_BEGIN) + r".*?" + re.escape(MARKER_END),
314
+ re.DOTALL,
315
+ )
316
+ new_text = pattern.sub(
317
+ block.strip(),
318
+ text,
319
+ )
320
+ if not new_text.endswith("\n"):
321
+ new_text += "\n"
322
+ else:
323
+ new_text = text.rstrip() + "\n" + block
324
+ # backup once
325
+ bak = home() / "backups" / f"{rc.name}.bak"
326
+ bak.parent.mkdir(parents=True, exist_ok=True)
327
+ if rc.exists():
328
+ shutil.copy2(rc, bak)
329
+ rc.write_text(new_text, encoding="utf-8")
330
+ updated.append(rc)
331
+ return updated
332
+
333
+
334
+ def list_cmd_rows(projects: list | None = None) -> list[dict]:
335
+ from ccmd.core.projects import ProjectRegistry
336
+
337
+ projects = list(projects) if projects is not None else ProjectRegistry().list()
338
+ shorts = allocate_shorts(projects)
339
+ rows = []
340
+ for p in projects:
341
+ s = shorts.get(p.id, "")
342
+ rows.append(
343
+ {
344
+ "short": s,
345
+ "project": p.id,
346
+ "default_agent": p.default_agent,
347
+ "path": p.path,
348
+ "variants": {a: f"{s}{suf}" for a, suf in AGENT_SUFFIX.items()},
349
+ }
350
+ )
351
+ rows.sort(key=lambda r: r["short"])
352
+ return rows
@@ -193,9 +193,12 @@ class HubScreen(Screen):
193
193
  ready = "ok" if a["installed"] else "no"
194
194
  roster.append(f" {mark} {i} {a['logo']} {a['id']:<8} {ready}")
195
195
 
196
+ short = p.short or p.id[:4]
196
197
  lines = [
197
198
  p.name,
198
199
  f"id {p.id}",
200
+ f"short {short} → {short}c claude · {short}g grok · {short}x codex · {short}u cursor",
201
+ f" {short}o goose · {short}a aider · {short}p chatgpt · bare {short} = default",
199
202
  f"path {p.path}",
200
203
  f"root {_root_label(p.path)}",
201
204
  f"exists {exists}",
@@ -209,6 +212,7 @@ class HubScreen(Screen):
209
212
  *roster,
210
213
  "",
211
214
  "enter open · d detach · b/f add from disk",
215
+ "shell: source ~/.ccmd/shell_cmds.sh (or: ccmd shell install)",
212
216
  ]
213
217
  detail.update("\n".join(lines))
214
218
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccmd
3
- Version: 2.0.5
3
+ Version: 2.0.7
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
@@ -24,6 +24,7 @@ ccmd/core/profiles.py
24
24
  ccmd/core/projects.py
25
25
  ccmd/core/scan.py
26
26
  ccmd/core/sessions.py
27
+ ccmd/core/shell_cmds.py
27
28
  ccmd/core/ssh.py
28
29
  ccmd/core/agents/__init__.py
29
30
  ccmd/core/agents/aider.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ccmd"
7
- version = "2.0.5"
7
+ version = "2.0.7"
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