@orderful/droid 0.7.0 → 0.8.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +80 -89
  3. package/assets/droid+claude.png +0 -0
  4. package/dist/commands/setup.d.ts +1 -0
  5. package/dist/commands/setup.d.ts.map +1 -1
  6. package/dist/commands/setup.js +77 -9
  7. package/dist/commands/setup.js.map +1 -1
  8. package/dist/commands/tui.d.ts.map +1 -1
  9. package/dist/commands/tui.js +111 -70
  10. package/dist/commands/tui.js.map +1 -1
  11. package/dist/lib/agents.d.ts +19 -4
  12. package/dist/lib/agents.d.ts.map +1 -1
  13. package/dist/lib/agents.js +121 -42
  14. package/dist/lib/agents.js.map +1 -1
  15. package/dist/lib/skills.d.ts.map +1 -1
  16. package/dist/lib/skills.js +55 -0
  17. package/dist/lib/skills.js.map +1 -1
  18. package/dist/lib/types.d.ts +1 -0
  19. package/dist/lib/types.d.ts.map +1 -1
  20. package/dist/skills/brain/SKILL.md +11 -9
  21. package/dist/skills/brain/SKILL.yaml +1 -1
  22. package/dist/skills/brain/commands/brain.md +9 -4
  23. package/dist/skills/brain/references/workflows.md +14 -4
  24. package/dist/skills/code-review/SKILL.md +57 -0
  25. package/dist/skills/code-review/SKILL.yaml +22 -0
  26. package/dist/skills/code-review/agents/edi-standards-reviewer/AGENT.md +39 -0
  27. package/dist/skills/code-review/agents/edi-standards-reviewer/AGENT.yaml +14 -0
  28. package/dist/skills/code-review/agents/error-handling-reviewer/AGENT.md +51 -0
  29. package/dist/skills/code-review/agents/error-handling-reviewer/AGENT.yaml +14 -0
  30. package/dist/skills/code-review/agents/test-coverage-analyzer/AGENT.md +53 -0
  31. package/dist/skills/code-review/agents/test-coverage-analyzer/AGENT.yaml +14 -0
  32. package/dist/skills/code-review/agents/type-reviewer/AGENT.md +50 -0
  33. package/dist/skills/code-review/agents/type-reviewer/AGENT.yaml +13 -0
  34. package/dist/skills/code-review/commands/code-review.md +91 -0
  35. package/dist/skills/comments/SKILL.md +20 -5
  36. package/dist/skills/comments/SKILL.yaml +1 -1
  37. package/dist/skills/comments/commands/comments.md +1 -1
  38. package/dist/skills/project/SKILL.md +9 -7
  39. package/dist/skills/project/SKILL.yaml +1 -1
  40. package/dist/skills/project/commands/project.md +9 -4
  41. package/dist/skills/project/references/creating.md +9 -4
  42. package/dist/skills/project/references/loading.md +11 -5
  43. package/package.json +1 -1
  44. package/src/commands/setup.test.ts +276 -0
  45. package/src/commands/setup.ts +80 -10
  46. package/src/commands/tui.tsx +149 -82
  47. package/src/lib/agents.ts +134 -44
  48. package/src/lib/skills.ts +60 -0
  49. package/src/lib/types.ts +1 -0
  50. package/src/skills/brain/SKILL.md +11 -9
  51. package/src/skills/brain/SKILL.yaml +1 -1
  52. package/src/skills/brain/commands/brain.md +9 -4
  53. package/src/skills/brain/references/workflows.md +14 -4
  54. package/src/skills/code-review/SKILL.md +57 -0
  55. package/src/skills/code-review/SKILL.yaml +22 -0
  56. package/src/skills/code-review/agents/edi-standards-reviewer/AGENT.md +39 -0
  57. package/src/skills/code-review/agents/edi-standards-reviewer/AGENT.yaml +14 -0
  58. package/src/skills/code-review/agents/error-handling-reviewer/AGENT.md +51 -0
  59. package/src/skills/code-review/agents/error-handling-reviewer/AGENT.yaml +14 -0
  60. package/src/skills/code-review/agents/test-coverage-analyzer/AGENT.md +53 -0
  61. package/src/skills/code-review/agents/test-coverage-analyzer/AGENT.yaml +14 -0
  62. package/src/skills/code-review/agents/type-reviewer/AGENT.md +50 -0
  63. package/src/skills/code-review/agents/type-reviewer/AGENT.yaml +13 -0
  64. package/src/skills/code-review/commands/code-review.md +91 -0
  65. package/src/skills/comments/SKILL.md +20 -5
  66. package/src/skills/comments/SKILL.yaml +1 -1
  67. package/src/skills/comments/commands/comments.md +1 -1
  68. package/src/skills/project/SKILL.md +9 -7
  69. package/src/skills/project/SKILL.yaml +1 -1
  70. package/src/skills/project/commands/project.md +9 -4
  71. package/src/skills/project/references/creating.md +9 -4
  72. package/src/skills/project/references/loading.md +11 -5
@@ -13,9 +13,6 @@ import {
13
13
  uninstallSkill,
14
14
  updateSkill,
15
15
  getSkillUpdateStatus,
16
- isCommandInstalled,
17
- installCommand,
18
- uninstallCommand,
19
16
  } from '../lib/skills.js';
20
17
  import { getBundledAgents, getBundledAgentsDir, isAgentInstalled, installAgent, uninstallAgent, type AgentManifest } from '../lib/agents.js';
21
18
  import { configExists, loadConfig, saveConfig, loadSkillOverrides, saveSkillOverrides } from '../lib/config.js';
@@ -133,6 +130,56 @@ function getOutputOptions(): Array<{ label: string; value: OutputPreference }> {
133
130
  return options;
134
131
  }
135
132
 
133
+ /**
134
+ * Check if an agent belongs to a skill bundle (whether installed or not)
135
+ * Returns the skill name if so, null otherwise
136
+ */
137
+ function getAgentBundledSkill(agentName: string): string | null {
138
+ const skillsDir = getBundledSkillsDir();
139
+ if (!existsSync(skillsDir)) return null;
140
+
141
+ const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
142
+ .filter((d) => d.isDirectory())
143
+ .map((d) => d.name);
144
+
145
+ for (const skillName of skillDirs) {
146
+ const agentDir = join(skillsDir, skillName, 'agents', agentName);
147
+ if (existsSync(agentDir)) {
148
+ return skillName;
149
+ }
150
+ }
151
+ return null;
152
+ }
153
+
154
+ /**
155
+ * Find the source path for an agent's AGENT.md
156
+ * Checks both standalone agents dir and skill-bundled agents
157
+ */
158
+ function findAgentSourcePath(agentName: string): string | null {
159
+ // Check standalone agents first
160
+ const standalonePath = join(getBundledAgentsDir(), agentName, 'AGENT.md');
161
+ if (existsSync(standalonePath)) {
162
+ return standalonePath;
163
+ }
164
+
165
+ // Check skill-bundled agents
166
+ const skillsDir = getBundledSkillsDir();
167
+ if (existsSync(skillsDir)) {
168
+ const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
169
+ .filter((d) => d.isDirectory())
170
+ .map((d) => d.name);
171
+
172
+ for (const skillName of skillDirs) {
173
+ const skillAgentPath = join(skillsDir, skillName, 'agents', agentName, 'AGENT.md');
174
+ if (existsSync(skillAgentPath)) {
175
+ return skillAgentPath;
176
+ }
177
+ }
178
+ }
179
+
180
+ return null;
181
+ }
182
+
136
183
  interface WelcomeScreenProps {
137
184
  onContinue: () => void;
138
185
  onUpdate: () => void;
@@ -461,13 +508,14 @@ function SetupScreen({ onComplete, onSkip, initialConfig }: SetupScreenProps) {
461
508
 
462
509
  function TabBar({ tabs, activeTab }: { tabs: { id: Tab; label: string }[]; activeTab: Tab }) {
463
510
  return (
464
- <Box flexDirection="row">
511
+ <Box flexDirection="row" flexWrap="wrap">
465
512
  {tabs.map((tab) => (
466
513
  <Text
467
514
  key={tab.id}
468
515
  backgroundColor={tab.id === activeTab ? colors.primary : undefined}
469
516
  color={tab.id === activeTab ? '#ffffff' : colors.textMuted}
470
517
  bold={tab.id === activeTab}
518
+ wrap="truncate"
471
519
  >
472
520
  {' '}{tab.label}{' '}
473
521
  </Text>
@@ -648,20 +696,9 @@ function CommandDetails({
648
696
  }
649
697
 
650
698
  const skillInstalled = isSkillInstalled(command.skillName);
651
- const installed = isCommandInstalled(command.name, command.skillName);
652
699
 
653
- // If skill is installed, command comes with it - no standalone uninstall
654
- const actions = skillInstalled
655
- ? [{ id: 'view', label: 'View', variant: 'default' }]
656
- : installed
657
- ? [
658
- { id: 'view', label: 'View', variant: 'default' },
659
- { id: 'uninstall', label: 'Uninstall', variant: 'danger' },
660
- ]
661
- : [
662
- { id: 'view', label: 'View', variant: 'default' },
663
- { id: 'install', label: 'Install', variant: 'primary' },
664
- ];
700
+ // Commands belong to skills - only show View, install via skill
701
+ const actions = [{ id: 'view', label: 'View', variant: 'default' }];
665
702
 
666
703
  return (
667
704
  <Box flexDirection="column" paddingLeft={2} flexGrow={1}>
@@ -670,8 +707,7 @@ function CommandDetails({
670
707
  <Box marginTop={1}>
671
708
  <Text color={colors.textDim}>
672
709
  from {command.skillName}
673
- {skillInstalled && <Text color={colors.success}> · via skill</Text>}
674
- {!skillInstalled && installed && <Text color={colors.success}> · installed</Text>}
710
+ {skillInstalled && <Text color={colors.success}> · installed</Text>}
675
711
  </Text>
676
712
  </Box>
677
713
 
@@ -747,18 +783,24 @@ function AgentDetails({
747
783
  }
748
784
 
749
785
  const installed = isAgentInstalled(agent.name);
786
+ const bundledSkill = getAgentBundledSkill(agent.name);
787
+ const skillInstalled = bundledSkill ? isSkillInstalled(bundledSkill) : false;
750
788
  const statusDisplay = agent.status === 'alpha' ? '[alpha]' : agent.status === 'beta' ? '[beta]' : '';
751
789
  const modeDisplay = agent.mode === 'primary' ? 'primary' : agent.mode === 'all' ? 'primary/subagent' : 'subagent';
752
790
 
753
- const actions = installed
754
- ? [
755
- { id: 'view', label: 'View', variant: 'default' },
756
- { id: 'uninstall', label: 'Uninstall', variant: 'danger' },
757
- ]
758
- : [
759
- { id: 'view', label: 'View', variant: 'default' },
760
- { id: 'install', label: 'Install', variant: 'primary' },
761
- ];
791
+ // If agent belongs to a skill, only show View (install via skill)
792
+ // Otherwise show Install/Uninstall for standalone agents
793
+ const actions = bundledSkill
794
+ ? [{ id: 'view', label: 'View', variant: 'default' }]
795
+ : installed
796
+ ? [
797
+ { id: 'view', label: 'View', variant: 'default' },
798
+ { id: 'uninstall', label: 'Uninstall', variant: 'danger' },
799
+ ]
800
+ : [
801
+ { id: 'view', label: 'View', variant: 'default' },
802
+ { id: 'install', label: 'Install', variant: 'primary' },
803
+ ];
762
804
 
763
805
  return (
764
806
  <Box flexDirection="column" paddingLeft={2} flexGrow={1}>
@@ -769,7 +811,9 @@ function AgentDetails({
769
811
  v{agent.version}
770
812
  {statusDisplay && <Text> · {statusDisplay}</Text>}
771
813
  {' · '}{modeDisplay}
772
- {installed && <Text color={colors.success}> · installed</Text>}
814
+ {bundledSkill && <Text color={colors.textMuted}> · from {bundledSkill}</Text>}
815
+ {skillInstalled && <Text color={colors.success}> · installed</Text>}
816
+ {!bundledSkill && installed && <Text color={colors.success}> · installed</Text>}
773
817
  </Text>
774
818
  </Box>
775
819
 
@@ -1017,6 +1061,8 @@ interface SkillConfigScreenProps {
1017
1061
  onCancel: () => void;
1018
1062
  }
1019
1063
 
1064
+ const MAX_VISIBLE_CONFIG_ITEMS = 4; // Each config item takes ~3 lines
1065
+
1020
1066
  function SkillConfigScreen({ skill, onComplete, onCancel }: SkillConfigScreenProps) {
1021
1067
  const configSchema = skill.config_schema || {};
1022
1068
  const configKeys = Object.keys(configSchema);
@@ -1034,11 +1080,15 @@ function SkillConfigScreen({ skill, onComplete, onCancel }: SkillConfigScreenPro
1034
1080
  });
1035
1081
 
1036
1082
  const [selectedIndex, setSelectedIndex] = useState(0);
1083
+ const [scrollOffset, setScrollOffset] = useState(0);
1037
1084
  const [editingField, setEditingField] = useState<string | null>(null);
1038
1085
  const [editValue, setEditValue] = useState('');
1039
1086
  const [editingSelect, setEditingSelect] = useState<string | null>(null);
1040
1087
  const [selectOptionIndex, setSelectOptionIndex] = useState(0);
1041
1088
 
1089
+ // Total items = config keys + Save button
1090
+ const totalItems = configKeys.length + 1;
1091
+
1042
1092
  const handleSave = () => {
1043
1093
  saveSkillOverrides(skill.name, values);
1044
1094
  onComplete();
@@ -1090,11 +1140,25 @@ function SkillConfigScreen({ skill, onComplete, onCancel }: SkillConfigScreenPro
1090
1140
  }
1091
1141
 
1092
1142
  if (key.upArrow) {
1093
- setSelectedIndex((prev) => Math.max(0, prev - 1));
1143
+ setSelectedIndex((prev) => {
1144
+ const newIndex = Math.max(0, prev - 1);
1145
+ // Scroll up if needed
1146
+ if (newIndex < scrollOffset) {
1147
+ setScrollOffset(newIndex);
1148
+ }
1149
+ return newIndex;
1150
+ });
1094
1151
  }
1095
1152
  if (key.downArrow) {
1096
1153
  // +1 for the Save button at the end
1097
- setSelectedIndex((prev) => Math.min(configKeys.length, prev + 1));
1154
+ setSelectedIndex((prev) => {
1155
+ const newIndex = Math.min(totalItems - 1, prev + 1);
1156
+ // Scroll down if needed
1157
+ if (newIndex >= scrollOffset + MAX_VISIBLE_CONFIG_ITEMS) {
1158
+ setScrollOffset(newIndex - MAX_VISIBLE_CONFIG_ITEMS + 1);
1159
+ }
1160
+ return newIndex;
1161
+ });
1098
1162
  }
1099
1163
 
1100
1164
  if (key.return) {
@@ -1145,6 +1209,13 @@ function SkillConfigScreen({ skill, onComplete, onCancel }: SkillConfigScreenPro
1145
1209
  );
1146
1210
  }
1147
1211
 
1212
+ // Calculate visible range
1213
+ const visibleEndIndex = Math.min(scrollOffset + MAX_VISIBLE_CONFIG_ITEMS, totalItems);
1214
+ const visibleConfigKeys = configKeys.slice(scrollOffset, Math.min(scrollOffset + MAX_VISIBLE_CONFIG_ITEMS, configKeys.length));
1215
+ const showSaveButton = visibleEndIndex > configKeys.length || scrollOffset + MAX_VISIBLE_CONFIG_ITEMS > configKeys.length;
1216
+ const showTopIndicator = scrollOffset > 0;
1217
+ const showBottomIndicator = scrollOffset + MAX_VISIBLE_CONFIG_ITEMS < totalItems;
1218
+
1148
1219
  return (
1149
1220
  <Box flexDirection="column" padding={1}>
1150
1221
  <Box marginBottom={1}>
@@ -1159,9 +1230,17 @@ function SkillConfigScreen({ skill, onComplete, onCancel }: SkillConfigScreenPro
1159
1230
  </Box>
1160
1231
 
1161
1232
  <Box flexDirection="column">
1162
- {configKeys.map((key, index) => {
1233
+ {/* Scroll up indicator */}
1234
+ {showTopIndicator && (
1235
+ <Box marginBottom={1}>
1236
+ <Text color={colors.textDim}> ↑ {scrollOffset} more</Text>
1237
+ </Box>
1238
+ )}
1239
+
1240
+ {visibleConfigKeys.map((key) => {
1241
+ const actualIndex = configKeys.indexOf(key);
1163
1242
  const option = configSchema[key];
1164
- const isSelected = selectedIndex === index;
1243
+ const isSelected = selectedIndex === actualIndex;
1165
1244
  const isEditing = editingField === key;
1166
1245
 
1167
1246
  return (
@@ -1213,19 +1292,28 @@ function SkillConfigScreen({ skill, onComplete, onCancel }: SkillConfigScreenPro
1213
1292
  );
1214
1293
  })}
1215
1294
 
1216
- {/* Save button */}
1217
- <Box marginTop={1}>
1218
- <Text>
1219
- <Text color={colors.textDim}>{selectedIndex === configKeys.length ? '>' : ' '} </Text>
1220
- <Text
1221
- backgroundColor={selectedIndex === configKeys.length ? colors.primary : undefined}
1222
- color={selectedIndex === configKeys.length ? '#ffffff' : colors.textMuted}
1223
- bold={selectedIndex === configKeys.length}
1224
- >
1225
- {' '}Save{' '}
1295
+ {/* Save button - show if in visible range */}
1296
+ {showSaveButton && (
1297
+ <Box marginTop={1}>
1298
+ <Text>
1299
+ <Text color={colors.textDim}>{selectedIndex === configKeys.length ? '>' : ' '} </Text>
1300
+ <Text
1301
+ backgroundColor={selectedIndex === configKeys.length ? colors.primary : undefined}
1302
+ color={selectedIndex === configKeys.length ? '#ffffff' : colors.textMuted}
1303
+ bold={selectedIndex === configKeys.length}
1304
+ >
1305
+ {' '}Save{' '}
1306
+ </Text>
1226
1307
  </Text>
1227
- </Text>
1228
- </Box>
1308
+ </Box>
1309
+ )}
1310
+
1311
+ {/* Scroll down indicator */}
1312
+ {showBottomIndicator && (
1313
+ <Box marginTop={1}>
1314
+ <Text color={colors.textDim}> ↓ {totalItems - scrollOffset - MAX_VISIBLE_CONFIG_ITEMS} more</Text>
1315
+ </Box>
1316
+ )}
1229
1317
  </Box>
1230
1318
 
1231
1319
  <Box marginTop={1}>
@@ -1329,7 +1417,10 @@ function App() {
1329
1417
  }
1330
1418
  if (key.downArrow) {
1331
1419
  const maxIndex =
1332
- activeTab === 'skills' ? skills.length - 1 : activeTab === 'commands' ? commands.length - 1 : 0;
1420
+ activeTab === 'skills' ? skills.length - 1
1421
+ : activeTab === 'commands' ? commands.length - 1
1422
+ : activeTab === 'agents' ? agents.length - 1
1423
+ : 0;
1333
1424
  setSelectedIndex((prev) => {
1334
1425
  const newIndex = Math.min(maxIndex, prev + 1);
1335
1426
  // Scroll down if needed
@@ -1440,16 +1531,17 @@ function App() {
1440
1531
  const agent = agents[selectedIndex];
1441
1532
  if (agent) {
1442
1533
  const installed = isAgentInstalled(agent.name);
1534
+ const bundledSkill = getAgentBundledSkill(agent.name);
1443
1535
  if (selectedAction === 0) {
1444
1536
  // View
1445
- const agentMdPath = join(getBundledAgentsDir(), agent.name, 'AGENT.md');
1446
- if (existsSync(agentMdPath)) {
1537
+ const agentMdPath = findAgentSourcePath(agent.name);
1538
+ if (agentMdPath) {
1447
1539
  const content = readFileSync(agentMdPath, 'utf-8');
1448
1540
  setReadmeContent({ title: `${agent.name}/AGENT.md`, content });
1449
1541
  setView('readme');
1450
1542
  }
1451
- } else if (installed && selectedAction === 1) {
1452
- // Uninstall
1543
+ } else if (!bundledSkill && installed && selectedAction === 1) {
1544
+ // Uninstall (only for standalone agents)
1453
1545
  const result = uninstallAgent(agent.name);
1454
1546
  setMessage({
1455
1547
  text: result.success ? `✓ Uninstalled ${agent.name}` : `✗ ${result.message}`,
@@ -1459,8 +1551,8 @@ function App() {
1459
1551
  setView('menu');
1460
1552
  setSelectedAction(0);
1461
1553
  }
1462
- } else if (!installed && selectedAction === 1) {
1463
- // Install
1554
+ } else if (!bundledSkill && !installed && selectedAction === 1) {
1555
+ // Install (only for standalone agents)
1464
1556
  const result = installAgent(agent.name);
1465
1557
  setMessage({
1466
1558
  text: result.success ? `✓ ${result.message}` : `✗ ${result.message}`,
@@ -1476,42 +1568,17 @@ function App() {
1476
1568
  if (key.return && activeTab === 'commands') {
1477
1569
  const command = commands[selectedIndex];
1478
1570
  if (command) {
1479
- const installed = isCommandInstalled(command.name, command.skillName);
1480
1571
  // Command file: extract part after skill name (e.g., "comments check" → "check.md")
1481
1572
  const cmdPart = command.name.startsWith(command.skillName + ' ')
1482
1573
  ? command.name.slice(command.skillName.length + 1)
1483
1574
  : command.name;
1484
1575
  const commandMdPath = join(getBundledSkillsDir(), command.skillName, 'commands', `${cmdPart}.md`);
1485
1576
 
1486
- if (selectedAction === 0) {
1487
- // View
1488
- if (existsSync(commandMdPath)) {
1489
- const content = readFileSync(commandMdPath, 'utf-8');
1490
- setReadmeContent({ title: `/${command.name}`, content });
1491
- setView('readme');
1492
- }
1493
- } else if (installed && selectedAction === 1) {
1494
- // Uninstall
1495
- const result = uninstallCommand(command.name, command.skillName);
1496
- setMessage({
1497
- text: result.success ? `✓ ${result.message}` : `✗ ${result.message}`,
1498
- type: result.success ? 'success' : 'error',
1499
- });
1500
- if (result.success) {
1501
- setView('menu');
1502
- setSelectedAction(0);
1503
- }
1504
- } else if (!installed && selectedAction === 1) {
1505
- // Install
1506
- const result = installCommand(command.name, command.skillName);
1507
- setMessage({
1508
- text: result.success ? `✓ ${result.message}` : `✗ ${result.message}`,
1509
- type: result.success ? 'success' : 'error',
1510
- });
1511
- if (result.success) {
1512
- setView('menu');
1513
- setSelectedAction(0);
1514
- }
1577
+ // Only View action available - commands install via skills
1578
+ if (selectedAction === 0 && existsSync(commandMdPath)) {
1579
+ const content = readFileSync(commandMdPath, 'utf-8');
1580
+ setReadmeContent({ title: `/${command.name}`, content });
1581
+ setView('readme');
1515
1582
  }
1516
1583
  }
1517
1584
  }
package/src/lib/agents.ts CHANGED
@@ -3,10 +3,23 @@ import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { homedir } from 'os';
5
5
  import YAML from 'yaml';
6
+ import { loadConfig } from './config.js';
7
+ import { AITool } from './types.js';
6
8
 
7
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
10
  const BUNDLED_AGENTS_DIR = join(__dirname, '../agents');
9
- const INSTALLED_AGENTS_DIR = join(homedir(), '.claude', 'agents');
11
+
12
+ /**
13
+ * Get the installation path for agents based on AI tool
14
+ */
15
+ export function getAgentsInstallPath(aiTool: AITool): string {
16
+ switch (aiTool) {
17
+ case AITool.ClaudeCode:
18
+ return join(homedir(), '.claude', 'agents');
19
+ case AITool.OpenCode:
20
+ return join(homedir(), '.config', 'opencode', 'agent');
21
+ }
22
+ }
10
23
 
11
24
  /**
12
25
  * Agent manifest structure
@@ -17,6 +30,8 @@ export interface AgentManifest {
17
30
  version: string;
18
31
  status?: 'alpha' | 'beta' | 'stable';
19
32
  mode?: 'primary' | 'subagent' | 'all'; // How the agent is used
33
+ model?: string; // Model to use (e.g., 'sonnet', 'opus', 'haiku')
34
+ color?: string; // Display color (e.g., 'purple', 'blue', 'green')
20
35
  tools?: string[]; // Allowed tools for this agent
21
36
  triggers?: string[];
22
37
  persona?: string;
@@ -48,23 +63,49 @@ export function loadAgentManifest(agentDir: string): AgentManifest | null {
48
63
  }
49
64
 
50
65
  /**
51
- * Get all bundled agents
66
+ * Get all bundled agents (standalone + skill-bundled)
52
67
  */
53
68
  export function getBundledAgents(): AgentManifest[] {
54
- if (!existsSync(BUNDLED_AGENTS_DIR)) {
55
- return [];
69
+ const agents: AgentManifest[] = [];
70
+ const seenNames = new Set<string>();
71
+
72
+ // Get standalone agents from src/agents/
73
+ if (existsSync(BUNDLED_AGENTS_DIR)) {
74
+ const agentDirs = readdirSync(BUNDLED_AGENTS_DIR, { withFileTypes: true })
75
+ .filter((dirent) => dirent.isDirectory())
76
+ .map((dirent) => dirent.name);
77
+
78
+ for (const agentDir of agentDirs) {
79
+ const manifest = loadAgentManifest(join(BUNDLED_AGENTS_DIR, agentDir));
80
+ if (manifest && !seenNames.has(manifest.name)) {
81
+ agents.push(manifest);
82
+ seenNames.add(manifest.name);
83
+ }
84
+ }
56
85
  }
57
86
 
58
- const agentDirs = readdirSync(BUNDLED_AGENTS_DIR, { withFileTypes: true })
59
- .filter((dirent) => dirent.isDirectory())
60
- .map((dirent) => dirent.name);
87
+ // Get skill-bundled agents from src/skills/*/agents/
88
+ const skillsDir = join(__dirname, '../skills');
89
+ if (existsSync(skillsDir)) {
90
+ const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
91
+ .filter((dirent) => dirent.isDirectory())
92
+ .map((dirent) => dirent.name);
61
93
 
62
- const agents: AgentManifest[] = [];
94
+ for (const skillName of skillDirs) {
95
+ const skillAgentsDir = join(skillsDir, skillName, 'agents');
96
+ if (!existsSync(skillAgentsDir)) continue;
97
+
98
+ const agentDirs = readdirSync(skillAgentsDir, { withFileTypes: true })
99
+ .filter((dirent) => dirent.isDirectory())
100
+ .map((dirent) => dirent.name);
63
101
 
64
- for (const agentDir of agentDirs) {
65
- const manifest = loadAgentManifest(join(BUNDLED_AGENTS_DIR, agentDir));
66
- if (manifest) {
67
- agents.push(manifest);
102
+ for (const agentDir of agentDirs) {
103
+ const manifest = loadAgentManifest(join(skillAgentsDir, agentDir));
104
+ if (manifest && !seenNames.has(manifest.name)) {
105
+ agents.push(manifest);
106
+ seenNames.add(manifest.name);
107
+ }
108
+ }
68
109
  }
69
110
  }
70
111
 
@@ -87,26 +128,74 @@ export function getAgentStatusDisplay(status?: string): string {
87
128
  }
88
129
 
89
130
  /**
90
- * Get installed agents directory
131
+ * Get installed agents directory for the configured AI tool
91
132
  */
92
133
  export function getInstalledAgentsDir(): string {
93
- return INSTALLED_AGENTS_DIR;
134
+ const config = loadConfig();
135
+ return getAgentsInstallPath(config.ai_tool);
94
136
  }
95
137
 
96
138
  /**
97
139
  * Check if an agent is installed
98
140
  */
99
141
  export function isAgentInstalled(agentName: string): boolean {
100
- const agentPath = join(INSTALLED_AGENTS_DIR, `${agentName}.md`);
142
+ const config = loadConfig();
143
+ const agentsDir = getAgentsInstallPath(config.ai_tool);
144
+ const agentPath = join(agentsDir, `${agentName}.md`);
101
145
  return existsSync(agentPath);
102
146
  }
103
147
 
104
148
  /**
105
- * Install an agent to ~/.claude/agents/
106
- * Combines AGENT.yaml metadata with AGENT.md content into a single .md file
149
+ * Generate Claude Code agent frontmatter
107
150
  */
108
- export function installAgent(agentName: string): { success: boolean; message: string } {
109
- const agentDir = join(BUNDLED_AGENTS_DIR, agentName);
151
+ function generateClaudeCodeAgent(manifest: AgentManifest, agentContent: string): string {
152
+ const lines = [
153
+ '---',
154
+ `name: ${manifest.name}`,
155
+ `description: ${manifest.description}`,
156
+ ];
157
+
158
+ if (manifest.tools && manifest.tools.length > 0) {
159
+ lines.push(`tools: ${manifest.tools.join(', ')}`);
160
+ }
161
+ if (manifest.color) {
162
+ lines.push(`color: ${manifest.color}`);
163
+ }
164
+
165
+ lines.push('---', '', agentContent.trim(), '');
166
+ return lines.join('\n');
167
+ }
168
+
169
+ /**
170
+ * Generate OpenCode agent frontmatter
171
+ * OpenCode uses a different format with explicit tool permissions
172
+ */
173
+ function generateOpenCodeAgent(manifest: AgentManifest, agentContent: string): string {
174
+ const lines = [
175
+ '---',
176
+ `description: ${manifest.description}`,
177
+ `mode: ${manifest.mode || 'subagent'}`,
178
+ ];
179
+
180
+ // OpenCode uses explicit tool permissions
181
+ if (manifest.tools && manifest.tools.length > 0) {
182
+ lines.push('tools:');
183
+ for (const tool of manifest.tools) {
184
+ const toolName = tool.toLowerCase();
185
+ lines.push(` ${toolName}: true`);
186
+ }
187
+ }
188
+
189
+ lines.push('---', '', agentContent.trim(), '');
190
+ return lines.join('\n');
191
+ }
192
+
193
+ /**
194
+ * Install an agent from a specific path
195
+ * Generates the appropriate format based on the configured AI tool
196
+ */
197
+ export function installAgentFromPath(agentDir: string, agentName: string): { success: boolean; message: string } {
198
+ const config = loadConfig();
110
199
  const manifestPath = join(agentDir, 'AGENT.yaml');
111
200
  const contentPath = join(agentDir, 'AGENT.md');
112
201
 
@@ -132,46 +221,47 @@ export function installAgent(agentName: string): { success: boolean; message: st
132
221
  agentContent = manifest.persona;
133
222
  }
134
223
 
135
- // Build frontmatter for Claude Code agent format
136
- const frontmatter: Record<string, unknown> = {
137
- name: manifest.name,
138
- description: manifest.description,
139
- };
140
-
141
- // Add tools if specified
142
- if (manifest.tools && manifest.tools.length > 0) {
143
- frontmatter.tools = manifest.tools.join(', ');
144
- }
145
-
146
- // Generate the installed agent file
147
- const installedContent = `---
148
- name: ${frontmatter.name}
149
- description: ${frontmatter.description}${frontmatter.tools ? `\ntools: ${frontmatter.tools}` : ''}
150
- ---
151
-
152
- ${agentContent.trim()}
153
- `;
224
+ // Generate format based on AI tool
225
+ const installedContent = config.ai_tool === AITool.ClaudeCode
226
+ ? generateClaudeCodeAgent(manifest, agentContent)
227
+ : generateOpenCodeAgent(manifest, agentContent);
154
228
 
155
229
  // Ensure agents directory exists
156
- if (!existsSync(INSTALLED_AGENTS_DIR)) {
157
- mkdirSync(INSTALLED_AGENTS_DIR, { recursive: true });
230
+ const agentsDir = getAgentsInstallPath(config.ai_tool);
231
+ if (!existsSync(agentsDir)) {
232
+ mkdirSync(agentsDir, { recursive: true });
158
233
  }
159
234
 
160
235
  // Write the agent file
161
- const outputPath = join(INSTALLED_AGENTS_DIR, `${agentName}.md`);
236
+ const outputPath = join(agentsDir, `${agentName}.md`);
162
237
  writeFileSync(outputPath, installedContent);
163
238
 
164
- return { success: true, message: `Installed ${agentName} to ~/.claude/agents/` };
239
+ const targetDir = config.ai_tool === AITool.ClaudeCode
240
+ ? '~/.claude/agents/'
241
+ : '~/.config/opencode/agent/';
242
+
243
+ return { success: true, message: `Installed ${agentName} to ${targetDir}` };
165
244
  } catch (error) {
166
245
  return { success: false, message: `Failed to install agent: ${error}` };
167
246
  }
168
247
  }
169
248
 
170
249
  /**
171
- * Uninstall an agent from ~/.claude/agents/
250
+ * Install an agent from the bundled agents directory
251
+ * Combines AGENT.yaml metadata with AGENT.md content into a single .md file
252
+ */
253
+ export function installAgent(agentName: string): { success: boolean; message: string } {
254
+ const agentDir = join(BUNDLED_AGENTS_DIR, agentName);
255
+ return installAgentFromPath(agentDir, agentName);
256
+ }
257
+
258
+ /**
259
+ * Uninstall an agent from the agents directory
172
260
  */
173
261
  export function uninstallAgent(agentName: string): { success: boolean; message: string } {
174
- const agentPath = join(INSTALLED_AGENTS_DIR, `${agentName}.md`);
262
+ const config = loadConfig();
263
+ const agentsDir = getAgentsInstallPath(config.ai_tool);
264
+ const agentPath = join(agentsDir, `${agentName}.md`);
175
265
 
176
266
  if (!existsSync(agentPath)) {
177
267
  return { success: false, message: `Agent not installed: ${agentName}` };