@orderful/droid 0.3.0 → 0.4.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.
- package/.claude/CLAUDE.md +41 -0
- package/CHANGELOG.md +23 -0
- package/dist/agents/README.md +137 -0
- package/dist/commands/tui.d.ts.map +1 -1
- package/dist/commands/tui.js +273 -16
- package/dist/commands/tui.js.map +1 -1
- package/dist/lib/agents.d.ts +53 -0
- package/dist/lib/agents.d.ts.map +1 -0
- package/dist/lib/agents.js +149 -0
- package/dist/lib/agents.js.map +1 -0
- package/dist/lib/skills.d.ts +20 -0
- package/dist/lib/skills.d.ts.map +1 -1
- package/dist/lib/skills.js +102 -0
- package/dist/lib/skills.js.map +1 -1
- package/dist/skills/README.md +85 -0
- package/dist/skills/comments/commands/README.md +58 -0
- package/package.json +2 -2
- package/src/agents/README.md +137 -0
- package/src/commands/tui.tsx +454 -21
- package/src/lib/agents.ts +186 -0
- package/src/lib/skills.ts +125 -0
- package/src/skills/README.md +85 -0
- package/src/skills/comments/commands/README.md +58 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Project instructions for Claude Code when working in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
Droid is a TUI dashboard for managing AI skills, commands, and agents. It installs content to Claude Code (`~/.claude/`) and OpenCode (`~/.config/opencode/`).
|
|
8
|
+
|
|
9
|
+
## Build & Test
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm run build # Compile TypeScript and copy skills/agents to dist/
|
|
13
|
+
npm run dev # Watch mode
|
|
14
|
+
npm run test # Run tests
|
|
15
|
+
npm run start # Run the TUI (bun dist/bin/droid.js)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Git Rules
|
|
19
|
+
|
|
20
|
+
- **NEVER push to main** without explicit user permission
|
|
21
|
+
- Always create a feature branch and PR for changes
|
|
22
|
+
- Branch naming: `tf/<ticket-or-description>` (e.g., `tf/agents-commands-tui`)
|
|
23
|
+
- Use changesets for version bumps: `npm run changeset`
|
|
24
|
+
|
|
25
|
+
## Structure
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
src/
|
|
29
|
+
├── bin/ # CLI entry point
|
|
30
|
+
├── commands/ # CLI commands (tui.tsx is the main dashboard)
|
|
31
|
+
├── lib/ # Core logic (skills.ts, agents.ts, config.ts)
|
|
32
|
+
├── skills/ # Bundled skills (copied to dist on build)
|
|
33
|
+
└── agents/ # Bundled agents (copied to dist on build)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Key Patterns
|
|
37
|
+
|
|
38
|
+
- Skills install to `~/.claude/skills/` and register in CLAUDE.md
|
|
39
|
+
- Commands install to `~/.claude/commands/`
|
|
40
|
+
- Agents install to `~/.claude/agents/`
|
|
41
|
+
- TUI uses Ink (React for CLI)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @orderful/droid
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`e7686f1`](https://github.com/Orderful/droid/commit/e7686f18615ff4730efef1cfe9440c268d957500) Thanks [@frytyler](https://github.com/frytyler)! - Add agents and commands install/uninstall functionality
|
|
8
|
+
|
|
9
|
+
**Agents:**
|
|
10
|
+
- New Agents tab in TUI with install/uninstall support
|
|
11
|
+
- Agents install to `~/.claude/agents/{name}.md`
|
|
12
|
+
- New schema fields: `mode` (primary/subagent/all), `tools` (allowed tools list)
|
|
13
|
+
- Sample `code-reviewer` agent included
|
|
14
|
+
|
|
15
|
+
**Commands:**
|
|
16
|
+
- Commands can now be installed independently of their parent skill
|
|
17
|
+
- Install copies command to `~/.claude/commands/`
|
|
18
|
+
- Smart handling: commands installed via skill show "via skill" and can't be uninstalled independently
|
|
19
|
+
|
|
20
|
+
**TUI Improvements:**
|
|
21
|
+
- View action for skills, commands, and agents with markdown syntax highlighting
|
|
22
|
+
- Fixed Enter key navigation for commands and agents tabs
|
|
23
|
+
- Scrolling fixes in markdown viewer
|
|
24
|
+
- Contributor README files for skills, commands, and agents
|
|
25
|
+
|
|
3
26
|
## 0.3.0
|
|
4
27
|
|
|
5
28
|
### Minor Changes
|
|
@@ -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":"
|
|
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"}
|
package/dist/commands/tui.js
CHANGED
|
@@ -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: '
|
|
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
|
-
: [
|
|
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
|
|
232
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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: "
|
|
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)
|