@orderful/droid 0.3.0 → 0.4.1

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,137 @@
1
+ # Contributing Agents
2
+
3
+ Agents are specialized AI personas with specific expertise or roles. They augment the AI's capabilities for particular tasks.
4
+
5
+ ## Directory Structure
6
+
7
+ Each agent is a directory containing:
8
+
9
+ ```
10
+ agents/
11
+ └── my-agent/
12
+ ├── AGENT.yaml # Required: Manifest with metadata and persona
13
+ └── AGENT.md # Required: Documentation for users
14
+ ```
15
+
16
+ ## AGENT.yaml (Manifest)
17
+
18
+ ```yaml
19
+ name: my-agent # Must match directory name
20
+ description: Short description # Shown in TUI and listings
21
+ version: 1.0.0 # Semver
22
+ status: alpha # alpha | beta | stable (optional)
23
+
24
+ # Agent type (required)
25
+ mode: subagent # primary | subagent | all
26
+
27
+ # Tools this agent can use (optional)
28
+ tools:
29
+ - Read
30
+ - Glob
31
+ - Grep
32
+ - Bash
33
+
34
+ # Suggested triggers - phrases that might invoke this agent
35
+ triggers:
36
+ - "review my code"
37
+ - "check for bugs"
38
+
39
+ # Persona - system prompt additions for this agent (used if AGENT.md is minimal)
40
+ persona: |
41
+ You are a specialized assistant focused on...
42
+
43
+ Your priorities are:
44
+ 1. First priority
45
+ 2. Second priority
46
+
47
+ Always be specific and constructive.
48
+ ```
49
+
50
+ ### Agent Modes
51
+
52
+ - **`primary`** - Main agents you interact with directly. Cycle through them with Tab key.
53
+ - **`subagent`** - Specialized helpers invoked via @mention (e.g., `@code-reviewer check this`) or by primary agents.
54
+ - **`all`** - Agent works as both primary and subagent.
55
+
56
+ ## AGENT.md (Documentation)
57
+
58
+ The AGENT.md file documents the agent for users:
59
+
60
+ ```markdown
61
+ ---
62
+ name: my-agent
63
+ description: Short description (must match AGENT.yaml)
64
+ ---
65
+
66
+ # My Agent
67
+
68
+ Description of what this agent does and when to use it.
69
+
70
+ ## What It Does
71
+
72
+ - Bullet points of capabilities
73
+ - What it focuses on
74
+ - What it ignores
75
+
76
+ ## Usage
77
+
78
+ Examples of how to invoke or use this agent.
79
+
80
+ ## Output Format
81
+
82
+ Description of how the agent formats its responses.
83
+ ```
84
+
85
+ ## Example Agent
86
+
87
+ See `code-reviewer/` for a complete example:
88
+
89
+ - Reviews code for bugs, security issues, style
90
+ - Categorizes issues by severity (critical/warning/suggestion)
91
+ - References specific file and line numbers
92
+
93
+ ## Ideas for Agents
94
+
95
+ - **test-writer** - Generates unit tests for code
96
+ - **doc-writer** - Writes documentation and comments
97
+ - **refactorer** - Suggests and applies refactoring patterns
98
+ - **security-auditor** - Deep security analysis
99
+ - **performance-optimizer** - Identifies performance bottlenecks
100
+
101
+ ## Installation
102
+
103
+ When you install an agent via the TUI, droid:
104
+
105
+ 1. Reads `AGENT.yaml` for metadata (name, description, tools)
106
+ 2. Reads `AGENT.md` for the agent's instructions/persona
107
+ 3. Combines them into a single `.md` file with frontmatter
108
+ 4. Writes to `~/.claude/agents/{name}.md`
109
+
110
+ The installed format matches what Claude Code expects:
111
+
112
+ ```markdown
113
+ ---
114
+ name: my-agent
115
+ description: Short description
116
+ tools: Read, Glob, Grep, Bash
117
+ ---
118
+
119
+ [Content from AGENT.md]
120
+ ```
121
+
122
+ ## Testing Your Agent
123
+
124
+ 1. Run `npm run build` to compile
125
+ 2. Run `droid` to open the TUI
126
+ 3. Navigate to Agents tab
127
+ 4. Verify your agent appears with correct metadata
128
+ 5. Install and verify it appears in `~/.claude/agents/`
129
+
130
+ ## Checklist
131
+
132
+ - [ ] `AGENT.yaml` has all required fields (name, description, version, mode)
133
+ - [ ] `AGENT.md` contains the agent's instructions/persona
134
+ - [ ] Name matches directory name
135
+ - [ ] Description is clear and concise
136
+ - [ ] Mode is appropriate (subagent for specialized tasks, primary for main assistants)
137
+ - [ ] Tools list only includes what the agent needs
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../../src/commands/tui.tsx"],"names":[],"mappings":"AAkkCA,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAWhD"}
1
+ {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../../src/commands/tui.tsx"],"names":[],"mappings":"AAm/CA,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAWhD"}
@@ -5,7 +5,8 @@ import { useState, useMemo } from 'react';
5
5
  import { execSync } from 'child_process';
6
6
  import { existsSync, readdirSync, readFileSync } from 'fs';
7
7
  import { join } from 'path';
8
- import { getBundledSkills, getBundledSkillsDir, isSkillInstalled, getInstalledSkill, installSkill, uninstallSkill, } from '../lib/skills.js';
8
+ import { getBundledSkills, getBundledSkillsDir, isSkillInstalled, getInstalledSkill, installSkill, uninstallSkill, isCommandInstalled, installCommand, uninstallCommand, } from '../lib/skills.js';
9
+ import { getBundledAgents, getBundledAgentsDir, isAgentInstalled, installAgent, uninstallAgent } from '../lib/agents.js';
9
10
  import { configExists, loadConfig, saveConfig, loadSkillOverrides, saveSkillOverrides } from '../lib/config.js';
10
11
  import { configureAIToolPermissions } from './setup.js';
11
12
  import { AITool, BuiltInOutput, ConfigOptionType } from '../lib/types.js';
@@ -210,10 +211,14 @@ function SkillDetails({ skill, isFocused, selectedAction, }) {
210
211
  const skillCommands = getCommandsFromSkills().filter((c) => c.skillName === skill.name);
211
212
  const actions = installed
212
213
  ? [
213
- { id: 'uninstall', label: 'Uninstall', variant: 'danger' },
214
+ { id: 'view', label: 'View', variant: 'default' },
214
215
  { id: 'configure', label: 'Configure', variant: 'primary' },
216
+ { id: 'uninstall', label: 'Uninstall', variant: 'danger' },
215
217
  ]
216
- : [{ id: 'install', label: 'Install', variant: 'primary' }];
218
+ : [
219
+ { id: 'view', label: 'View', variant: 'default' },
220
+ { id: 'install', label: 'Install', variant: 'primary' },
221
+ ];
217
222
  return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexGrow: 1, children: [_jsx(Text, { color: colors.text, bold: true, children: skill.name }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, children: [skill.version, skill.status && ` · ${skill.status}`, installed && _jsx(Text, { color: colors.success, children: " \u00B7 installed" })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: skill.description }) }), skillCommands.length > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, children: ["Commands: ", skillCommands.map((c) => `/${c.name}`).join(', ')] }) })), skill.examples && skill.examples.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.textDim, children: ["Examples", skill.examples.length > 2 ? ` (showing 2 of ${skill.examples.length})` : '', ":"] }), skill.examples.slice(0, 2).map((example, i) => (_jsxs(Box, { flexDirection: "column", marginTop: i > 0 ? 1 : 0, children: [_jsxs(Text, { color: colors.textMuted, children: [" ", example.title] }), example.code
218
223
  .trim()
219
224
  .split('\n')
@@ -224,12 +229,143 @@ function SkillDetails({ skill, isFocused, selectedAction, }) {
224
229
  : colors.primary
225
230
  : colors.bgSelected, color: selectedAction === index ? '#ffffff' : colors.textMuted, bold: selectedAction === index, children: [' ', action.label, ' '] }, action.id))) }))] }));
226
231
  }
227
- function CommandDetails({ command }) {
232
+ function CommandDetails({ command, isFocused, selectedAction, }) {
228
233
  if (!command) {
229
234
  return (_jsx(Box, { paddingLeft: 2, flexGrow: 1, children: _jsx(Text, { color: colors.textDim, children: "Select a command" }) }));
230
235
  }
231
- const installed = isSkillInstalled(command.skillName);
232
- return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexGrow: 1, children: [_jsxs(Text, { color: colors.text, bold: true, children: ["/", command.name] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, children: ["from ", command.skillName, installed && _jsx(Text, { color: colors.success, children: " \u00B7 installed" })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: command.description }) }), command.usage.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: colors.textDim, children: "Usage:" }), command.usage.map((u, i) => (_jsxs(Text, { color: colors.textMuted, children: [' ', u] }, i)))] }))] }));
236
+ const skillInstalled = isSkillInstalled(command.skillName);
237
+ const installed = isCommandInstalled(command.name, command.skillName);
238
+ // If skill is installed, command comes with it - no standalone uninstall
239
+ const actions = skillInstalled
240
+ ? [{ id: 'view', label: 'View', variant: 'default' }]
241
+ : installed
242
+ ? [
243
+ { id: 'view', label: 'View', variant: 'default' },
244
+ { id: 'uninstall', label: 'Uninstall', variant: 'danger' },
245
+ ]
246
+ : [
247
+ { id: 'view', label: 'View', variant: 'default' },
248
+ { id: 'install', label: 'Install', variant: 'primary' },
249
+ ];
250
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexGrow: 1, children: [_jsxs(Text, { color: colors.text, bold: true, children: ["/", command.name] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, children: ["from ", command.skillName, skillInstalled && _jsx(Text, { color: colors.success, children: " \u00B7 via skill" }), !skillInstalled && installed && _jsx(Text, { color: colors.success, children: " \u00B7 installed" })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: command.description }) }), command.usage.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: colors.textDim, children: "Usage:" }), command.usage.map((u, i) => (_jsxs(Text, { color: colors.textMuted, children: [' ', u] }, i)))] })), isFocused && (_jsx(Box, { flexDirection: "row", marginTop: 1, children: actions.map((action, index) => (_jsxs(Text, { backgroundColor: selectedAction === index
251
+ ? action.variant === 'danger'
252
+ ? colors.error
253
+ : colors.primary
254
+ : colors.bgSelected, color: selectedAction === index ? '#ffffff' : colors.textMuted, bold: selectedAction === index, children: [' ', action.label, ' '] }, action.id))) }))] }));
255
+ }
256
+ function AgentItem({ agent, isSelected }) {
257
+ const statusDisplay = agent.status === 'alpha' ? '[alpha]' : agent.status === 'beta' ? '[beta]' : '';
258
+ return (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: colors.textDim, children: [isSelected ? '>' : ' ', " "] }), _jsx(Text, { color: isSelected ? colors.text : colors.textMuted, children: agent.name }), _jsxs(Text, { color: colors.textDim, children: [" v", agent.version] }), statusDisplay && _jsxs(Text, { color: colors.textDim, children: [" ", statusDisplay] })] }) }));
259
+ }
260
+ function AgentDetails({ agent, isFocused, selectedAction, }) {
261
+ if (!agent) {
262
+ return (_jsx(Box, { paddingLeft: 2, flexGrow: 1, children: _jsx(Text, { color: colors.textDim, children: "Select an agent" }) }));
263
+ }
264
+ const installed = isAgentInstalled(agent.name);
265
+ const statusDisplay = agent.status === 'alpha' ? '[alpha]' : agent.status === 'beta' ? '[beta]' : '';
266
+ const modeDisplay = agent.mode === 'primary' ? 'primary' : agent.mode === 'all' ? 'primary/subagent' : 'subagent';
267
+ const actions = installed
268
+ ? [
269
+ { id: 'view', label: 'View', variant: 'default' },
270
+ { id: 'uninstall', label: 'Uninstall', variant: 'danger' },
271
+ ]
272
+ : [
273
+ { id: 'view', label: 'View', variant: 'default' },
274
+ { id: 'install', label: 'Install', variant: 'primary' },
275
+ ];
276
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexGrow: 1, children: [_jsx(Text, { color: colors.text, bold: true, children: agent.name }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, children: ["v", agent.version, statusDisplay && _jsxs(Text, { children: [" \u00B7 ", statusDisplay] }), ' · ', modeDisplay, installed && _jsx(Text, { color: colors.success, children: " \u00B7 installed" })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: agent.description }) }), agent.tools && agent.tools.length > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, children: ["Tools: ", agent.tools.join(', ')] }) })), agent.triggers && agent.triggers.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: colors.textDim, children: "Triggers:" }), agent.triggers.slice(0, 3).map((trigger, i) => (_jsxs(Text, { color: colors.textMuted, children: [' ', "\"", trigger, "\""] }, i)))] })), isFocused && (_jsx(Box, { flexDirection: "row", marginTop: 1, children: actions.map((action, index) => (_jsxs(Text, { backgroundColor: selectedAction === index
277
+ ? action.variant === 'danger'
278
+ ? colors.error
279
+ : colors.primary
280
+ : colors.bgSelected, color: selectedAction === index ? '#ffffff' : colors.textMuted, bold: selectedAction === index, children: [' ', action.label, ' '] }, action.id))) }))] }));
281
+ }
282
+ function MarkdownLine({ line, inCodeBlock }) {
283
+ // Code block content
284
+ if (inCodeBlock) {
285
+ return _jsx(Text, { color: "#a5d6ff", children: line || ' ' });
286
+ }
287
+ // Code block delimiter
288
+ if (line.startsWith('```')) {
289
+ return _jsx(Text, { color: colors.textDim, children: line });
290
+ }
291
+ // Headers
292
+ if (line.startsWith('# ')) {
293
+ return _jsx(Text, { color: colors.text, bold: true, children: line.slice(2) });
294
+ }
295
+ if (line.startsWith('## ')) {
296
+ return _jsx(Text, { color: colors.text, bold: true, children: line.slice(3) });
297
+ }
298
+ if (line.startsWith('### ')) {
299
+ return _jsx(Text, { color: "#c9d1d9", bold: true, children: line.slice(4) });
300
+ }
301
+ // YAML frontmatter delimiter
302
+ if (line === '---') {
303
+ return _jsx(Text, { color: colors.textDim, children: line });
304
+ }
305
+ // List items
306
+ if (line.match(/^[\s]*[-*]\s/)) {
307
+ return _jsx(Text, { color: colors.textMuted, children: line });
308
+ }
309
+ // Blockquotes
310
+ if (line.startsWith('>')) {
311
+ return _jsx(Text, { color: "#8b949e", italic: true, children: line });
312
+ }
313
+ // Table rows
314
+ if (line.includes('|')) {
315
+ return _jsx(Text, { color: colors.textMuted, children: line });
316
+ }
317
+ // Default
318
+ return _jsx(Text, { color: colors.textMuted, children: line || ' ' });
319
+ }
320
+ function ReadmeViewer({ title, content, onClose, }) {
321
+ const [scrollOffset, setScrollOffset] = useState(0);
322
+ const lines = useMemo(() => content.split('\n'), [content]);
323
+ const maxVisible = 20;
324
+ // Pre-compute code block state for each line
325
+ const lineStates = useMemo(() => {
326
+ const states = [];
327
+ let inCode = false;
328
+ for (const line of lines) {
329
+ if (line.startsWith('```')) {
330
+ states.push(false); // Delimiter itself is not "in" code block for styling
331
+ inCode = !inCode;
332
+ }
333
+ else {
334
+ states.push(inCode);
335
+ }
336
+ }
337
+ return states;
338
+ }, [lines]);
339
+ // Max offset: when at end, we have top indicator + (maxVisible-1) content lines
340
+ // So max offset is lines.length - (maxVisible - 1) = lines.length - maxVisible + 1
341
+ const maxOffset = Math.max(0, lines.length - maxVisible + 1);
342
+ useInput((input, key) => {
343
+ if (key.escape) {
344
+ onClose();
345
+ return;
346
+ }
347
+ if (key.upArrow) {
348
+ setScrollOffset((prev) => Math.max(0, prev - 1));
349
+ }
350
+ if (key.downArrow) {
351
+ setScrollOffset((prev) => Math.min(maxOffset, prev + 1));
352
+ }
353
+ if (key.pageDown || input === ' ') {
354
+ setScrollOffset((prev) => Math.min(maxOffset, prev + maxVisible));
355
+ }
356
+ if (key.pageUp) {
357
+ setScrollOffset((prev) => Math.max(0, prev - maxVisible));
358
+ }
359
+ });
360
+ // Adjust visible lines based on whether indicators are shown
361
+ const showTopIndicator = scrollOffset > 0;
362
+ // Reserve space for bottom indicator if not at end
363
+ const contentLines = maxVisible - (showTopIndicator ? 1 : 0);
364
+ const endIndex = Math.min(scrollOffset + contentLines, lines.length);
365
+ const showBottomIndicator = endIndex < lines.length;
366
+ const actualContentLines = contentLines - (showBottomIndicator ? 1 : 0);
367
+ const visibleLines = lines.slice(scrollOffset, scrollOffset + actualContentLines);
368
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: colors.text, bold: true, children: title }), _jsxs(Text, { color: colors.textDim, children: [" \u00B7 ", lines.length, " lines"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, children: [showTopIndicator && (_jsxs(Text, { color: colors.textDim, children: ["\u2191 ", scrollOffset, " more lines"] })), visibleLines.map((line, i) => (_jsx(MarkdownLine, { line: line, inCodeBlock: lineStates[scrollOffset + i] }, scrollOffset + i))), showBottomIndicator && (_jsxs(Text, { color: colors.textDim, children: ["\u2193 ", lines.length - scrollOffset - actualContentLines, " more lines"] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, children: "\u2191\u2193 scroll \u00B7 space/pgdn page \u00B7 esc back" }) })] }));
233
369
  }
234
370
  function SettingsDetails({ onEditSettings, isFocused, }) {
235
371
  const config = loadConfig();
@@ -328,9 +464,11 @@ function App() {
328
464
  const [scrollOffset, setScrollOffset] = useState(0);
329
465
  const [message, setMessage] = useState(null);
330
466
  const [isEditingSettings, setIsEditingSettings] = useState(false);
467
+ const [readmeContent, setReadmeContent] = useState(null);
331
468
  const MAX_VISIBLE_ITEMS = 6;
332
469
  const skills = getBundledSkills();
333
470
  const commands = getCommandsFromSkills();
471
+ const agents = getBundledAgents();
334
472
  useInput((input, key) => {
335
473
  if (message)
336
474
  setMessage(null);
@@ -382,6 +520,12 @@ function App() {
382
520
  if (activeTab === 'skills' && skills.length > 0) {
383
521
  setView('detail');
384
522
  }
523
+ else if (activeTab === 'commands' && commands.length > 0) {
524
+ setView('detail');
525
+ }
526
+ else if (activeTab === 'agents' && agents.length > 0) {
527
+ setView('detail');
528
+ }
385
529
  else if (activeTab === 'settings') {
386
530
  setIsEditingSettings(true);
387
531
  setView('setup');
@@ -397,16 +541,43 @@ function App() {
397
541
  setSelectedAction((prev) => Math.max(0, prev - 1));
398
542
  }
399
543
  if (key.rightArrow) {
400
- const skill = skills[selectedIndex];
401
- const installed = skill ? isSkillInstalled(skill.name) : false;
402
- const maxActions = installed ? 1 : 0;
544
+ let maxActions = 0;
545
+ if (activeTab === 'skills') {
546
+ const skill = skills[selectedIndex];
547
+ const installed = skill ? isSkillInstalled(skill.name) : false;
548
+ maxActions = installed ? 2 : 1; // View, Configure, Uninstall or View, Install
549
+ }
550
+ else if (activeTab === 'agents') {
551
+ maxActions = 1; // View, Install/Uninstall
552
+ }
553
+ else if (activeTab === 'commands') {
554
+ const command = commands[selectedIndex];
555
+ // If parent skill is installed, only View is available
556
+ const skillInstalled = command ? isSkillInstalled(command.skillName) : false;
557
+ maxActions = skillInstalled ? 0 : 1;
558
+ }
403
559
  setSelectedAction((prev) => Math.min(maxActions, prev + 1));
404
560
  }
405
561
  if (key.return && activeTab === 'skills') {
406
562
  const skill = skills[selectedIndex];
407
563
  if (skill) {
408
564
  const installed = isSkillInstalled(skill.name);
409
- if (installed && selectedAction === 0) {
565
+ // Actions: installed = [View, Configure, Uninstall], not installed = [View, Install]
566
+ if (selectedAction === 0) {
567
+ // View
568
+ const skillMdPath = join(getBundledSkillsDir(), skill.name, 'SKILL.md');
569
+ if (existsSync(skillMdPath)) {
570
+ const content = readFileSync(skillMdPath, 'utf-8');
571
+ setReadmeContent({ title: `${skill.name}/SKILL.md`, content });
572
+ setView('readme');
573
+ }
574
+ }
575
+ else if (installed && selectedAction === 1) {
576
+ // Configure
577
+ setView('configure');
578
+ }
579
+ else if (installed && selectedAction === 2) {
580
+ // Uninstall
410
581
  const result = uninstallSkill(skill.name);
411
582
  setMessage({
412
583
  text: result.success ? `✓ Uninstalled ${skill.name}` : `✗ ${result.message}`,
@@ -417,11 +588,8 @@ function App() {
417
588
  setSelectedAction(0);
418
589
  }
419
590
  }
420
- else if (installed && selectedAction === 1) {
421
- // Configure
422
- setView('configure');
423
- }
424
- else if (!installed) {
591
+ else if (!installed && selectedAction === 1) {
592
+ // Install
425
593
  const result = installSkill(skill.name);
426
594
  setMessage({
427
595
  text: result.success ? `✓ Installed ${skill.name}` : `✗ ${result.message}`,
@@ -434,10 +602,93 @@ function App() {
434
602
  }
435
603
  }
436
604
  }
605
+ if (key.return && activeTab === 'agents') {
606
+ const agent = agents[selectedIndex];
607
+ if (agent) {
608
+ const installed = isAgentInstalled(agent.name);
609
+ if (selectedAction === 0) {
610
+ // View
611
+ const agentMdPath = join(getBundledAgentsDir(), agent.name, 'AGENT.md');
612
+ if (existsSync(agentMdPath)) {
613
+ const content = readFileSync(agentMdPath, 'utf-8');
614
+ setReadmeContent({ title: `${agent.name}/AGENT.md`, content });
615
+ setView('readme');
616
+ }
617
+ }
618
+ else if (installed && selectedAction === 1) {
619
+ // Uninstall
620
+ const result = uninstallAgent(agent.name);
621
+ setMessage({
622
+ text: result.success ? `✓ Uninstalled ${agent.name}` : `✗ ${result.message}`,
623
+ type: result.success ? 'success' : 'error',
624
+ });
625
+ if (result.success) {
626
+ setView('menu');
627
+ setSelectedAction(0);
628
+ }
629
+ }
630
+ else if (!installed && selectedAction === 1) {
631
+ // Install
632
+ const result = installAgent(agent.name);
633
+ setMessage({
634
+ text: result.success ? `✓ ${result.message}` : `✗ ${result.message}`,
635
+ type: result.success ? 'success' : 'error',
636
+ });
637
+ if (result.success) {
638
+ setView('menu');
639
+ setSelectedAction(0);
640
+ }
641
+ }
642
+ }
643
+ }
644
+ if (key.return && activeTab === 'commands') {
645
+ const command = commands[selectedIndex];
646
+ if (command) {
647
+ const installed = isCommandInstalled(command.name, command.skillName);
648
+ // Command file: extract part after skill name (e.g., "comments check" → "check.md")
649
+ const cmdPart = command.name.startsWith(command.skillName + ' ')
650
+ ? command.name.slice(command.skillName.length + 1)
651
+ : command.name;
652
+ const commandMdPath = join(getBundledSkillsDir(), command.skillName, 'commands', `${cmdPart}.md`);
653
+ if (selectedAction === 0) {
654
+ // View
655
+ if (existsSync(commandMdPath)) {
656
+ const content = readFileSync(commandMdPath, 'utf-8');
657
+ setReadmeContent({ title: `/${command.name}`, content });
658
+ setView('readme');
659
+ }
660
+ }
661
+ else if (installed && selectedAction === 1) {
662
+ // Uninstall
663
+ const result = uninstallCommand(command.name, command.skillName);
664
+ setMessage({
665
+ text: result.success ? `✓ ${result.message}` : `✗ ${result.message}`,
666
+ type: result.success ? 'success' : 'error',
667
+ });
668
+ if (result.success) {
669
+ setView('menu');
670
+ setSelectedAction(0);
671
+ }
672
+ }
673
+ else if (!installed && selectedAction === 1) {
674
+ // Install
675
+ const result = installCommand(command.name, command.skillName);
676
+ setMessage({
677
+ text: result.success ? `✓ ${result.message}` : `✗ ${result.message}`,
678
+ type: result.success ? 'success' : 'error',
679
+ });
680
+ if (result.success) {
681
+ setView('menu');
682
+ setSelectedAction(0);
683
+ }
684
+ }
685
+ }
686
+ }
437
687
  }
438
688
  });
439
689
  const selectedSkill = activeTab === 'skills' ? skills[selectedIndex] ?? null : null;
440
690
  const selectedCommand = activeTab === 'commands' ? commands[selectedIndex] ?? null : null;
691
+ const selectedAgent = activeTab === 'agents' ? agents[selectedIndex] ?? null : null;
441
692
  if (view === 'welcome') {
442
693
  return (_jsx(WelcomeScreen, { onContinue: () => {
443
694
  // If no config exists, show setup first
@@ -458,6 +709,12 @@ function App() {
458
709
  setView('menu');
459
710
  }, initialConfig: isEditingSettings ? loadConfig() : undefined }));
460
711
  }
712
+ if (view === 'readme' && readmeContent) {
713
+ return (_jsx(ReadmeViewer, { title: readmeContent.title, content: readmeContent.content, onClose: () => {
714
+ setReadmeContent(null);
715
+ setView('detail');
716
+ } }));
717
+ }
461
718
  if (view === 'configure' && selectedSkill) {
462
719
  return (_jsx(SkillConfigScreen, { skill: selectedSkill, onComplete: () => {
463
720
  setMessage({ text: `✓ Configuration saved for ${selectedSkill.name}`, type: 'success' });
@@ -466,7 +723,7 @@ function App() {
466
723
  setView('detail');
467
724
  } }));
468
725
  }
469
- return (_jsxs(Box, { flexDirection: "row", padding: 1, children: [_jsxs(Box, { flexDirection: "column", width: 44, borderStyle: "single", borderColor: colors.border, children: [_jsx(Box, { paddingX: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "[" }), _jsx(Text, { color: colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: "] " }), _jsx(Text, { color: colors.textMuted, children: "droid" })] }) }), _jsx(Box, { paddingX: 1, marginTop: 1, children: _jsx(TabBar, { tabs: tabs, activeTab: activeTab }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [activeTab === 'skills' && (_jsxs(_Fragment, { children: [scrollOffset > 0 && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2191 ", scrollOffset, " more"] }) })), skills.slice(scrollOffset, scrollOffset + MAX_VISIBLE_ITEMS).map((skill, index) => (_jsx(SkillItem, { skill: skill, isSelected: scrollOffset + index === selectedIndex, isActive: scrollOffset + index === selectedIndex && view === 'detail' }, skill.name))), scrollOffset + MAX_VISIBLE_ITEMS < skills.length && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2193 ", skills.length - scrollOffset - MAX_VISIBLE_ITEMS, " more"] }) })), skills.length > MAX_VISIBLE_ITEMS && (_jsx(Box, { paddingX: 1, marginTop: 1, children: _jsxs(Text, { color: colors.textDim, children: [skills.length, " skills total"] }) }))] })), activeTab === 'commands' && (_jsxs(_Fragment, { children: [scrollOffset > 0 && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2191 ", scrollOffset, " more"] }) })), commands.slice(scrollOffset, scrollOffset + MAX_VISIBLE_ITEMS).map((cmd, index) => (_jsx(CommandItem, { command: cmd, isSelected: scrollOffset + index === selectedIndex }, cmd.name))), scrollOffset + MAX_VISIBLE_ITEMS < commands.length && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2193 ", commands.length - scrollOffset - MAX_VISIBLE_ITEMS, " more"] }) }))] })), activeTab === 'agents' && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: colors.textDim, children: "Coming soon" }) })), activeTab === 'settings' && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: colors.textDim, children: "View and edit config" }) }))] }), _jsx(Box, { paddingX: 1, marginTop: 1, children: _jsx(Text, { color: colors.textDim, children: view === 'menu' ? '←→ ↑↓ enter q' : '←→ enter esc q' }) })] }), activeTab === 'skills' && (_jsx(SkillDetails, { skill: selectedSkill, isFocused: view === 'detail', selectedAction: selectedAction })), activeTab === 'commands' && _jsx(CommandDetails, { command: selectedCommand }), activeTab === 'settings' && (_jsx(SettingsDetails, { onEditSettings: () => setView('setup'), isFocused: false })), activeTab === 'agents' && (_jsx(Box, { paddingLeft: 2, flexGrow: 1, children: _jsx(Text, { color: colors.textDim, children: "Coming soon" }) })), message && (_jsx(Box, { position: "absolute", marginTop: 12, children: _jsx(Text, { color: message.type === 'success' ? colors.success : colors.error, children: message.text }) }))] }));
726
+ return (_jsxs(Box, { flexDirection: "row", padding: 1, children: [_jsxs(Box, { flexDirection: "column", width: 44, borderStyle: "single", borderColor: colors.border, children: [_jsx(Box, { paddingX: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "[" }), _jsx(Text, { color: colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: "] " }), _jsx(Text, { color: colors.textMuted, children: "droid" })] }) }), _jsx(Box, { paddingX: 1, marginTop: 1, children: _jsx(TabBar, { tabs: tabs, activeTab: activeTab }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [activeTab === 'skills' && (_jsxs(_Fragment, { children: [scrollOffset > 0 && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2191 ", scrollOffset, " more"] }) })), skills.slice(scrollOffset, scrollOffset + MAX_VISIBLE_ITEMS).map((skill, index) => (_jsx(SkillItem, { skill: skill, isSelected: scrollOffset + index === selectedIndex, isActive: scrollOffset + index === selectedIndex && view === 'detail' }, skill.name))), scrollOffset + MAX_VISIBLE_ITEMS < skills.length && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2193 ", skills.length - scrollOffset - MAX_VISIBLE_ITEMS, " more"] }) })), skills.length > MAX_VISIBLE_ITEMS && (_jsx(Box, { paddingX: 1, marginTop: 1, children: _jsxs(Text, { color: colors.textDim, children: [skills.length, " skills total"] }) }))] })), activeTab === 'commands' && (_jsxs(_Fragment, { children: [scrollOffset > 0 && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2191 ", scrollOffset, " more"] }) })), commands.slice(scrollOffset, scrollOffset + MAX_VISIBLE_ITEMS).map((cmd, index) => (_jsx(CommandItem, { command: cmd, isSelected: scrollOffset + index === selectedIndex }, cmd.name))), scrollOffset + MAX_VISIBLE_ITEMS < commands.length && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2193 ", commands.length - scrollOffset - MAX_VISIBLE_ITEMS, " more"] }) }))] })), activeTab === 'agents' && (_jsxs(_Fragment, { children: [scrollOffset > 0 && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2191 ", scrollOffset, " more"] }) })), agents.length === 0 ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: colors.textDim, children: "No agents available" }) })) : (agents.slice(scrollOffset, scrollOffset + MAX_VISIBLE_ITEMS).map((agent, index) => (_jsx(AgentItem, { agent: agent, isSelected: scrollOffset + index === selectedIndex }, agent.name)))), scrollOffset + MAX_VISIBLE_ITEMS < agents.length && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2193 ", agents.length - scrollOffset - MAX_VISIBLE_ITEMS, " more"] }) }))] })), activeTab === 'settings' && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: colors.textDim, children: "View and edit config" }) }))] }), _jsx(Box, { paddingX: 1, marginTop: 1, children: _jsx(Text, { color: colors.textDim, children: view === 'menu' ? '←→ ↑↓ enter q' : '←→ enter esc q' }) })] }), activeTab === 'skills' && (_jsx(SkillDetails, { skill: selectedSkill, isFocused: view === 'detail', selectedAction: selectedAction })), activeTab === 'commands' && (_jsx(CommandDetails, { command: selectedCommand, isFocused: view === 'detail', selectedAction: selectedAction })), activeTab === 'settings' && (_jsx(SettingsDetails, { onEditSettings: () => setView('setup'), isFocused: false })), activeTab === 'agents' && (_jsx(AgentDetails, { agent: selectedAgent, isFocused: view === 'detail', selectedAction: selectedAction })), message && (_jsx(Box, { position: "absolute", marginTop: 12, children: _jsx(Text, { color: message.type === 'success' ? colors.success : colors.error, children: message.text }) }))] }));
470
727
  }
471
728
  export async function tuiCommand() {
472
729
  // Enter alternate screen (fullscreen mode)