codemaxxing 0.1.14 → 0.2.0

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.
@@ -0,0 +1,203 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { REGISTRY } from "../skills/registry.js";
5
+ const SKILLS_DIR = join(homedir(), ".codemaxxing", "skills");
6
+ /**
7
+ * Ensure the skills directory exists
8
+ */
9
+ function ensureSkillsDir() {
10
+ if (!existsSync(SKILLS_DIR)) {
11
+ mkdirSync(SKILLS_DIR, { recursive: true });
12
+ }
13
+ }
14
+ /**
15
+ * List all installed skills by scanning ~/.codemaxxing/skills/
16
+ */
17
+ export function listInstalledSkills() {
18
+ ensureSkillsDir();
19
+ const skills = [];
20
+ try {
21
+ const entries = readdirSync(SKILLS_DIR, { withFileTypes: true });
22
+ for (const entry of entries) {
23
+ if (!entry.isDirectory())
24
+ continue;
25
+ const metaPath = join(SKILLS_DIR, entry.name, "skill.json");
26
+ if (!existsSync(metaPath))
27
+ continue;
28
+ try {
29
+ const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
30
+ skills.push({
31
+ name: meta.name ?? entry.name,
32
+ description: meta.description ?? "",
33
+ version: meta.version ?? "0.0.0",
34
+ author: meta.author ?? "unknown",
35
+ tags: meta.tags ?? [],
36
+ });
37
+ }
38
+ catch {
39
+ // skip malformed skill.json
40
+ }
41
+ }
42
+ }
43
+ catch {
44
+ // directory doesn't exist or can't be read
45
+ }
46
+ return skills;
47
+ }
48
+ /**
49
+ * Install a skill from the built-in registry
50
+ */
51
+ export function installSkill(name) {
52
+ const skill = REGISTRY.find((s) => s.name === name);
53
+ if (!skill) {
54
+ return { ok: false, message: `Skill "${name}" not found in registry` };
55
+ }
56
+ ensureSkillsDir();
57
+ const skillDir = join(SKILLS_DIR, name);
58
+ if (existsSync(skillDir)) {
59
+ return { ok: false, message: `Skill "${name}" is already installed` };
60
+ }
61
+ mkdirSync(skillDir, { recursive: true });
62
+ mkdirSync(join(skillDir, "examples"), { recursive: true });
63
+ // Write skill.json
64
+ const meta = {
65
+ name: skill.name,
66
+ description: skill.description,
67
+ version: skill.version,
68
+ author: skill.author,
69
+ tags: skill.tags,
70
+ };
71
+ writeFileSync(join(skillDir, "skill.json"), JSON.stringify(meta, null, 2));
72
+ // Write prompt.md
73
+ writeFileSync(join(skillDir, "prompt.md"), skill.prompt);
74
+ return { ok: true, message: `Installed skill: ${skill.name}` };
75
+ }
76
+ /**
77
+ * Remove an installed skill
78
+ */
79
+ export function removeSkill(name) {
80
+ const skillDir = join(SKILLS_DIR, name);
81
+ if (!existsSync(skillDir)) {
82
+ return { ok: false, message: `Skill "${name}" is not installed` };
83
+ }
84
+ rmSync(skillDir, { recursive: true, force: true });
85
+ return { ok: true, message: `Removed skill: ${name}` };
86
+ }
87
+ /**
88
+ * Get the prompt.md content for an installed skill
89
+ */
90
+ export function getSkillPrompt(name) {
91
+ const promptPath = join(SKILLS_DIR, name, "prompt.md");
92
+ if (!existsSync(promptPath))
93
+ return null;
94
+ return readFileSync(promptPath, "utf-8");
95
+ }
96
+ /**
97
+ * Get examples from a skill's examples/ directory
98
+ */
99
+ function getSkillExamples(name) {
100
+ const examplesDir = join(SKILLS_DIR, name, "examples");
101
+ if (!existsSync(examplesDir))
102
+ return [];
103
+ try {
104
+ return readdirSync(examplesDir)
105
+ .filter((f) => f.endsWith(".md"))
106
+ .map((f) => readFileSync(join(examplesDir, f), "utf-8"));
107
+ }
108
+ catch {
109
+ return [];
110
+ }
111
+ }
112
+ /**
113
+ * Get skills that should be active for the given project directory.
114
+ * If .codemaxxing/skills.json exists in the project, only those skills are active.
115
+ * Otherwise, all installed skills are active.
116
+ */
117
+ export function getActiveSkills(cwd, sessionDisabled = new Set()) {
118
+ const installed = listInstalledSkills().map((s) => s.name);
119
+ let active;
120
+ const projectConfig = join(cwd, ".codemaxxing", "skills.json");
121
+ if (existsSync(projectConfig)) {
122
+ try {
123
+ const config = JSON.parse(readFileSync(projectConfig, "utf-8"));
124
+ const projectSkills = config.skills ?? [];
125
+ // Only include project skills that are actually installed
126
+ active = projectSkills.filter((s) => installed.includes(s));
127
+ }
128
+ catch {
129
+ active = installed;
130
+ }
131
+ }
132
+ else {
133
+ active = installed;
134
+ }
135
+ // Filter out session-disabled skills
136
+ return active.filter((s) => !sessionDisabled.has(s));
137
+ }
138
+ /**
139
+ * Build the skill prompt blocks to inject into the system prompt
140
+ */
141
+ export function buildSkillPrompts(cwd, sessionDisabled = new Set()) {
142
+ const activeSkills = getActiveSkills(cwd, sessionDisabled);
143
+ if (activeSkills.length === 0)
144
+ return "";
145
+ const blocks = [];
146
+ for (const name of activeSkills) {
147
+ const prompt = getSkillPrompt(name);
148
+ if (!prompt)
149
+ continue;
150
+ blocks.push(`\n--- Skill: ${name} ---`);
151
+ blocks.push(prompt.trim());
152
+ // Include examples if any
153
+ const examples = getSkillExamples(name);
154
+ for (const example of examples) {
155
+ blocks.push(`\n### Example:\n${example.trim()}`);
156
+ }
157
+ blocks.push(`--- End Skill ---`);
158
+ }
159
+ return blocks.join("\n");
160
+ }
161
+ /**
162
+ * Create a scaffold for a new custom skill
163
+ */
164
+ export function createSkillScaffold(name) {
165
+ ensureSkillsDir();
166
+ const skillDir = join(SKILLS_DIR, name);
167
+ if (existsSync(skillDir)) {
168
+ return { ok: false, message: `Skill "${name}" already exists` };
169
+ }
170
+ mkdirSync(skillDir, { recursive: true });
171
+ mkdirSync(join(skillDir, "examples"), { recursive: true });
172
+ const meta = {
173
+ name,
174
+ description: "A custom skill",
175
+ version: "1.0.0",
176
+ author: "you",
177
+ tags: [],
178
+ };
179
+ writeFileSync(join(skillDir, "skill.json"), JSON.stringify(meta, null, 2));
180
+ writeFileSync(join(skillDir, "prompt.md"), `# ${name}\n\nAdd your skill prompt here. This content will be injected into the system prompt.\n\n## Guidelines\n- Be specific and actionable\n- Include best practices\n- List anti-patterns to avoid\n`);
181
+ return { ok: true, message: `Created skill scaffold: ${name}`, path: skillDir };
182
+ }
183
+ /**
184
+ * Search the built-in registry by name, tags, or description
185
+ */
186
+ export function searchRegistry(query) {
187
+ const q = query.toLowerCase();
188
+ return REGISTRY.filter((s) => s.name.toLowerCase().includes(q) ||
189
+ s.description.toLowerCase().includes(q) ||
190
+ s.tags.some((t) => t.toLowerCase().includes(q)));
191
+ }
192
+ /**
193
+ * Return all skills from the built-in registry
194
+ */
195
+ export function getRegistrySkills() {
196
+ return REGISTRY;
197
+ }
198
+ /**
199
+ * Get the count of active skills
200
+ */
201
+ export function getActiveSkillCount(cwd, sessionDisabled = new Set()) {
202
+ return getActiveSkills(cwd, sessionDisabled).length;
203
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemaxxing",
3
- "version": "0.1.14",
3
+ "version": "0.2.0",
4
4
  "description": "Open-source terminal coding agent. Connect any LLM. Max your code.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/agent.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  import { FILE_TOOLS, executeTool, generateDiff, getExistingContent } from "./tools/files.js";
9
9
  import { buildProjectContext, getSystemPrompt } from "./utils/context.js";
10
10
  import { isGitRepo, autoCommit } from "./utils/git.js";
11
+ import { buildSkillPrompts, getActiveSkillCount } from "./utils/skills.js";
11
12
  import { createSession, saveMessage, updateTokenEstimate, updateSessionCost, loadMessages } from "./utils/sessions.js";
12
13
  import type { ProviderConfig } from "./config.js";
13
14
 
@@ -99,6 +100,7 @@ export class CodingAgent {
99
100
  private totalCost: number = 0;
100
101
  private systemPrompt: string = "";
101
102
  private compressionThreshold: number;
103
+ private sessionDisabledSkills: Set<string> = new Set();
102
104
 
103
105
  constructor(private options: AgentOptions) {
104
106
  this.providerType = options.provider.type || "openai";
@@ -128,7 +130,8 @@ export class CodingAgent {
128
130
  */
129
131
  async init(): Promise<void> {
130
132
  const context = await buildProjectContext(this.cwd);
131
- this.systemPrompt = await getSystemPrompt(context);
133
+ const skillPrompts = buildSkillPrompts(this.cwd, this.sessionDisabledSkills);
134
+ this.systemPrompt = await getSystemPrompt(context, skillPrompts);
132
135
 
133
136
  this.messages = [
134
137
  { role: "system", content: this.systemPrompt },
@@ -689,6 +692,26 @@ export class CodingAgent {
689
692
  };
690
693
  }
691
694
 
695
+ disableSkill(name: string): void {
696
+ this.sessionDisabledSkills.add(name);
697
+ }
698
+
699
+ enableSkill(name: string): void {
700
+ this.sessionDisabledSkills.delete(name);
701
+ }
702
+
703
+ getSessionDisabledSkills(): Set<string> {
704
+ return this.sessionDisabledSkills;
705
+ }
706
+
707
+ getActiveSkillCount(): number {
708
+ return getActiveSkillCount(this.cwd, this.sessionDisabledSkills);
709
+ }
710
+
711
+ getCwd(): string {
712
+ return this.cwd;
713
+ }
714
+
692
715
  reset(): void {
693
716
  const systemMsg = this.messages[0];
694
717
  this.messages = [systemMsg];
package/src/index.tsx CHANGED
@@ -11,6 +11,7 @@ import { execSync } from "child_process";
11
11
  import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./utils/git.js";
12
12
  import { getTheme, listThemes, THEMES, DEFAULT_THEME, type Theme } from "./themes.js";
13
13
  import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow, saveApiKey } from "./utils/auth.js";
14
+ import { listInstalledSkills, installSkill, removeSkill, getRegistrySkills, searchRegistry, createSkillScaffold, getActiveSkills, getActiveSkillCount } from "./utils/skills.js";
14
15
 
15
16
  const VERSION = "0.1.9";
16
17
 
@@ -46,6 +47,13 @@ const SLASH_COMMANDS = [
46
47
  { cmd: "/sessions", desc: "list past sessions" },
47
48
  { cmd: "/session delete", desc: "delete a session" },
48
49
  { cmd: "/resume", desc: "resume a past session" },
50
+ { cmd: "/skills", desc: "manage skill packs" },
51
+ { cmd: "/skills install", desc: "install a skill" },
52
+ { cmd: "/skills remove", desc: "remove a skill" },
53
+ { cmd: "/skills list", desc: "show installed skills" },
54
+ { cmd: "/skills search", desc: "search registry" },
55
+ { cmd: "/skills on", desc: "enable skill for session" },
56
+ { cmd: "/skills off", desc: "disable skill for session" },
49
57
  { cmd: "/quit", desc: "exit" },
50
58
  ];
51
59
 
@@ -144,6 +152,9 @@ function App() {
144
152
  const [loginPickerIndex, setLoginPickerIndex] = useState(0);
145
153
  const [loginMethodPicker, setLoginMethodPicker] = useState<{ provider: string; methods: string[] } | null>(null);
146
154
  const [loginMethodIndex, setLoginMethodIndex] = useState(0);
155
+ const [skillsPicker, setSkillsPicker] = useState<"menu" | "browse" | "installed" | "remove" | null>(null);
156
+ const [skillsPickerIndex, setSkillsPickerIndex] = useState(0);
157
+ const [sessionDisabledSkills, setSessionDisabledSkills] = useState<Set<string>>(new Set());
147
158
  const [approval, setApproval] = useState<{
148
159
  tool: string;
149
160
  args: Record<string, unknown>;
@@ -323,7 +334,9 @@ function App() {
323
334
  const selected = matches[idx];
324
335
  if (selected) {
325
336
  // Commands that need args (like /commit, /model) — fill input instead of executing
326
- if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete") {
337
+ if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete" ||
338
+ selected.cmd === "/skills install" || selected.cmd === "/skills remove" || selected.cmd === "/skills search" ||
339
+ selected.cmd === "/skills on" || selected.cmd === "/skills off") {
327
340
  setInput(selected.cmd + " ");
328
341
  setCmdIndex(0);
329
342
  setInputKey((k) => k + 1);
@@ -384,10 +397,94 @@ function App() {
384
397
  " /push — push to remote",
385
398
  " /git on — enable auto-commits",
386
399
  " /git off — disable auto-commits",
400
+ " /skills — manage skill packs",
387
401
  " /quit — exit",
388
402
  ].join("\n"));
389
403
  return;
390
404
  }
405
+ // ── Skills commands (work without agent) ──
406
+ if (trimmed === "/skills") {
407
+ setSkillsPicker("menu");
408
+ setSkillsPickerIndex(0);
409
+ return;
410
+ }
411
+ if (trimmed.startsWith("/skills install ")) {
412
+ const name = trimmed.replace("/skills install ", "").trim();
413
+ const result = installSkill(name);
414
+ addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
415
+ return;
416
+ }
417
+ if (trimmed.startsWith("/skills remove ")) {
418
+ const name = trimmed.replace("/skills remove ", "").trim();
419
+ const result = removeSkill(name);
420
+ addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
421
+ return;
422
+ }
423
+ if (trimmed === "/skills list") {
424
+ const installed = listInstalledSkills();
425
+ if (installed.length === 0) {
426
+ addMsg("info", "No skills installed. Use /skills to browse & install.");
427
+ } else {
428
+ const active = getActiveSkills(process.cwd(), sessionDisabledSkills);
429
+ const lines = installed.map((s) => {
430
+ const isActive = active.includes(s.name);
431
+ const disabledBySession = sessionDisabledSkills.has(s.name);
432
+ const status = disabledBySession ? " (off)" : isActive ? " (on)" : "";
433
+ return ` ${isActive ? "●" : "○"} ${s.name} — ${s.description}${status}`;
434
+ });
435
+ addMsg("info", `Installed skills:\n${lines.join("\n")}`);
436
+ }
437
+ return;
438
+ }
439
+ if (trimmed.startsWith("/skills search ")) {
440
+ const query = trimmed.replace("/skills search ", "").trim();
441
+ const results = searchRegistry(query);
442
+ if (results.length === 0) {
443
+ addMsg("info", `No skills found matching "${query}".`);
444
+ } else {
445
+ const installed = listInstalledSkills().map((s) => s.name);
446
+ const lines = results.map((s) => {
447
+ const mark = installed.includes(s.name) ? " ✓" : "";
448
+ return ` ${s.name} — ${s.description}${mark}`;
449
+ });
450
+ addMsg("info", `Registry matches:\n${lines.join("\n")}`);
451
+ }
452
+ return;
453
+ }
454
+ if (trimmed.startsWith("/skills create ")) {
455
+ const name = trimmed.replace("/skills create ", "").trim();
456
+ if (!name) {
457
+ addMsg("info", "Usage: /skills create <name>");
458
+ return;
459
+ }
460
+ const result = createSkillScaffold(name);
461
+ addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}\n Edit: ${result.path}/prompt.md` : `✗ ${result.message}`);
462
+ return;
463
+ }
464
+ if (trimmed.startsWith("/skills on ")) {
465
+ const name = trimmed.replace("/skills on ", "").trim();
466
+ const installed = listInstalledSkills().map((s) => s.name);
467
+ if (!installed.includes(name)) {
468
+ addMsg("error", `Skill "${name}" is not installed.`);
469
+ return;
470
+ }
471
+ setSessionDisabledSkills((prev) => { const next = new Set(prev); next.delete(name); return next; });
472
+ if (agent) agent.enableSkill(name);
473
+ addMsg("info", `✅ Enabled skill: ${name}`);
474
+ return;
475
+ }
476
+ if (trimmed.startsWith("/skills off ")) {
477
+ const name = trimmed.replace("/skills off ", "").trim();
478
+ const installed = listInstalledSkills().map((s) => s.name);
479
+ if (!installed.includes(name)) {
480
+ addMsg("error", `Skill "${name}" is not installed.`);
481
+ return;
482
+ }
483
+ setSessionDisabledSkills((prev) => { const next = new Set(prev); next.add(name); return next; });
484
+ if (agent) agent.disableSkill(name);
485
+ addMsg("info", `✅ Disabled skill: ${name} (session only)`);
486
+ return;
487
+ }
391
488
  if (trimmed.startsWith("/theme")) {
392
489
  const themeName = trimmed.replace("/theme", "").trim();
393
490
  if (!themeName) {
@@ -739,6 +836,148 @@ function App() {
739
836
  return;
740
837
  }
741
838
 
839
+ // Skills picker navigation
840
+ if (skillsPicker) {
841
+ if (skillsPicker === "menu") {
842
+ const menuItems = ["browse", "installed", "create", "remove"];
843
+ if (key.upArrow) {
844
+ setSkillsPickerIndex((prev) => (prev - 1 + menuItems.length) % menuItems.length);
845
+ return;
846
+ }
847
+ if (key.downArrow) {
848
+ setSkillsPickerIndex((prev) => (prev + 1) % menuItems.length);
849
+ return;
850
+ }
851
+ if (key.escape) {
852
+ setSkillsPicker(null);
853
+ return;
854
+ }
855
+ if (key.return) {
856
+ const selected = menuItems[skillsPickerIndex];
857
+ if (selected === "browse") {
858
+ setSkillsPicker("browse");
859
+ setSkillsPickerIndex(0);
860
+ } else if (selected === "installed") {
861
+ setSkillsPicker("installed");
862
+ setSkillsPickerIndex(0);
863
+ } else if (selected === "create") {
864
+ setSkillsPicker(null);
865
+ setInput("/skills create ");
866
+ setInputKey((k) => k + 1);
867
+ } else if (selected === "remove") {
868
+ const installed = listInstalledSkills();
869
+ if (installed.length === 0) {
870
+ setSkillsPicker(null);
871
+ addMsg("info", "No skills installed to remove.");
872
+ } else {
873
+ setSkillsPicker("remove");
874
+ setSkillsPickerIndex(0);
875
+ }
876
+ }
877
+ return;
878
+ }
879
+ return;
880
+ }
881
+ if (skillsPicker === "browse") {
882
+ const registry = getRegistrySkills();
883
+ if (key.upArrow) {
884
+ setSkillsPickerIndex((prev) => (prev - 1 + registry.length) % registry.length);
885
+ return;
886
+ }
887
+ if (key.downArrow) {
888
+ setSkillsPickerIndex((prev) => (prev + 1) % registry.length);
889
+ return;
890
+ }
891
+ if (key.escape) {
892
+ setSkillsPicker("menu");
893
+ setSkillsPickerIndex(0);
894
+ return;
895
+ }
896
+ if (key.return) {
897
+ const selected = registry[skillsPickerIndex];
898
+ if (selected) {
899
+ const result = installSkill(selected.name);
900
+ addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
901
+ }
902
+ setSkillsPicker(null);
903
+ return;
904
+ }
905
+ return;
906
+ }
907
+ if (skillsPicker === "installed") {
908
+ const installed = listInstalledSkills();
909
+ if (installed.length === 0) {
910
+ setSkillsPicker("menu");
911
+ setSkillsPickerIndex(0);
912
+ addMsg("info", "No skills installed.");
913
+ return;
914
+ }
915
+ if (key.upArrow) {
916
+ setSkillsPickerIndex((prev) => (prev - 1 + installed.length) % installed.length);
917
+ return;
918
+ }
919
+ if (key.downArrow) {
920
+ setSkillsPickerIndex((prev) => (prev + 1) % installed.length);
921
+ return;
922
+ }
923
+ if (key.escape) {
924
+ setSkillsPicker("menu");
925
+ setSkillsPickerIndex(0);
926
+ return;
927
+ }
928
+ if (key.return) {
929
+ // Toggle on/off for session
930
+ const selected = installed[skillsPickerIndex];
931
+ if (selected) {
932
+ const isDisabled = sessionDisabledSkills.has(selected.name);
933
+ if (isDisabled) {
934
+ setSessionDisabledSkills((prev) => { const next = new Set(prev); next.delete(selected.name); return next; });
935
+ if (agent) agent.enableSkill(selected.name);
936
+ addMsg("info", `✅ Enabled: ${selected.name}`);
937
+ } else {
938
+ setSessionDisabledSkills((prev) => { const next = new Set(prev); next.add(selected.name); return next; });
939
+ if (agent) agent.disableSkill(selected.name);
940
+ addMsg("info", `✅ Disabled: ${selected.name} (session only)`);
941
+ }
942
+ }
943
+ setSkillsPicker(null);
944
+ return;
945
+ }
946
+ return;
947
+ }
948
+ if (skillsPicker === "remove") {
949
+ const installed = listInstalledSkills();
950
+ if (installed.length === 0) {
951
+ setSkillsPicker(null);
952
+ return;
953
+ }
954
+ if (key.upArrow) {
955
+ setSkillsPickerIndex((prev) => (prev - 1 + installed.length) % installed.length);
956
+ return;
957
+ }
958
+ if (key.downArrow) {
959
+ setSkillsPickerIndex((prev) => (prev + 1) % installed.length);
960
+ return;
961
+ }
962
+ if (key.escape) {
963
+ setSkillsPicker("menu");
964
+ setSkillsPickerIndex(0);
965
+ return;
966
+ }
967
+ if (key.return) {
968
+ const selected = installed[skillsPickerIndex];
969
+ if (selected) {
970
+ const result = removeSkill(selected.name);
971
+ addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
972
+ }
973
+ setSkillsPicker(null);
974
+ return;
975
+ }
976
+ return;
977
+ }
978
+ return;
979
+ }
980
+
742
981
  // Theme picker navigation
743
982
  if (themePicker) {
744
983
  const themeKeys = listThemes();
@@ -1072,6 +1311,78 @@ function App() {
1072
1311
  </Box>
1073
1312
  )}
1074
1313
 
1314
+ {/* ═══ SKILLS PICKER ═══ */}
1315
+ {skillsPicker === "menu" && (
1316
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
1317
+ <Text bold color={theme.colors.secondary}>Skills:</Text>
1318
+ {[
1319
+ { key: "browse", label: "Browse & Install", icon: "📦" },
1320
+ { key: "installed", label: "Installed Skills", icon: "📋" },
1321
+ { key: "create", label: "Create Custom Skill", icon: "➕" },
1322
+ { key: "remove", label: "Remove Skill", icon: "🗑️" },
1323
+ ].map((item, i) => (
1324
+ <Text key={item.key}>
1325
+ {i === skillsPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
1326
+ <Text color={i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.primary} bold>{item.icon} {item.label}</Text>
1327
+ </Text>
1328
+ ))}
1329
+ <Text dimColor>{" ↑↓ navigate · Enter select · Esc cancel"}</Text>
1330
+ </Box>
1331
+ )}
1332
+ {skillsPicker === "browse" && (() => {
1333
+ const registry = getRegistrySkills();
1334
+ const installed = listInstalledSkills().map((s) => s.name);
1335
+ return (
1336
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
1337
+ <Text bold color={theme.colors.secondary}>Browse Skills Registry:</Text>
1338
+ {registry.map((s, i) => (
1339
+ <Text key={s.name}>
1340
+ {i === skillsPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
1341
+ <Text color={i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.primary} bold>{s.name}</Text>
1342
+ <Text color={theme.colors.muted}>{" — "}{s.description}</Text>
1343
+ {installed.includes(s.name) ? <Text color={theme.colors.success}> ✓</Text> : null}
1344
+ </Text>
1345
+ ))}
1346
+ <Text dimColor>{" ↑↓ navigate · Enter install · Esc back"}</Text>
1347
+ </Box>
1348
+ );
1349
+ })()}
1350
+ {skillsPicker === "installed" && (() => {
1351
+ const installed = listInstalledSkills();
1352
+ const active = getActiveSkills(process.cwd(), sessionDisabledSkills);
1353
+ return (
1354
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
1355
+ <Text bold color={theme.colors.secondary}>Installed Skills:</Text>
1356
+ {installed.length === 0 ? (
1357
+ <Text color={theme.colors.muted}> No skills installed. Use Browse & Install.</Text>
1358
+ ) : installed.map((s, i) => (
1359
+ <Text key={s.name}>
1360
+ {i === skillsPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
1361
+ <Text color={i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.primary} bold>{s.name}</Text>
1362
+ <Text color={theme.colors.muted}>{" — "}{s.description}</Text>
1363
+ {active.includes(s.name) ? <Text color={theme.colors.success}> (on)</Text> : <Text color={theme.colors.muted}> (off)</Text>}
1364
+ </Text>
1365
+ ))}
1366
+ <Text dimColor>{" ↑↓ navigate · Enter toggle · Esc back"}</Text>
1367
+ </Box>
1368
+ );
1369
+ })()}
1370
+ {skillsPicker === "remove" && (() => {
1371
+ const installed = listInstalledSkills();
1372
+ return (
1373
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.error} paddingX={1} marginBottom={0}>
1374
+ <Text bold color={theme.colors.error}>Remove a skill:</Text>
1375
+ {installed.map((s, i) => (
1376
+ <Text key={s.name}>
1377
+ {i === skillsPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
1378
+ <Text color={i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.muted}>{s.name} — {s.description}</Text>
1379
+ </Text>
1380
+ ))}
1381
+ <Text dimColor>{" ↑↓ navigate · Enter remove · Esc back"}</Text>
1382
+ </Box>
1383
+ );
1384
+ })()}
1385
+
1075
1386
  {/* ═══ THEME PICKER ═══ */}
1076
1387
  {themePicker && (
1077
1388
  <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
@@ -1182,6 +1493,10 @@ function App() {
1182
1493
  return "";
1183
1494
  })()}
1184
1495
  {modelName ? ` · 🤖 ${modelName}` : ""}
1496
+ {(() => {
1497
+ const count = getActiveSkillCount(process.cwd(), sessionDisabledSkills);
1498
+ return count > 0 ? ` · 🧠 ${count} skill${count !== 1 ? "s" : ""}` : "";
1499
+ })()}
1185
1500
  </Text>
1186
1501
  </Box>
1187
1502
  )}