So many changes. Three projects, nested libraries, and a working console app.
So many changes. Three projects, nested libraries, and a working console app.

--- /dev/null
+++ b/KerbalStuff/FormUpload.cs
@@ -1,1 +1,132 @@
+// Implements multipart/form-data POST in C# http://www.ietf.org/rfc/rfc2388.txt
+// http://www.briangrinstead.com/blog/multipart-form-post-in-c
 
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+
+public static class FormUpload
+{
+	private static readonly Encoding encoding = Encoding.UTF8;
+
+	public static HttpWebResponse MultipartFormDataPost(string postUrl, string userAgent, Dictionary<string, object> postParameters)
+	{
+		return MultipartFormDataPost(postUrl, userAgent, postParameters, new CookieContainer());
+	}
+
+	public static HttpWebResponse MultipartFormDataPost(string postUrl, string userAgent, Dictionary<string, object> postParameters, CookieContainer cookies)
+	{
+		string formDataBoundary = String.Format("----------{0:N}", Guid.NewGuid());
+		string contentType = "multipart/form-data; boundary=" + formDataBoundary;
+
+		byte[] formData = GetMultipartFormData(postParameters, formDataBoundary);
+
+		return PostForm(postUrl, userAgent, contentType, formData, cookies);
+	}
+
+	private static HttpWebResponse PostForm(string postUrl, string userAgent, string contentType, byte[] formData)
+	{
+		return PostForm(postUrl, userAgent, contentType, formData, new CookieContainer());
+	}
+
+	private static HttpWebResponse PostForm(string postUrl, string userAgent, string contentType, byte[] formData, CookieContainer cookies)
+	{
+		HttpWebRequest request = WebRequest.Create(postUrl) as HttpWebRequest;
+
+		if (request == null)
+		{
+			throw new NullReferenceException("request is not a http request");
+		}
+
+		// Set up the request properties.
+		request.Method = "POST";
+		request.ContentType = contentType;
+		request.UserAgent = userAgent;
+		request.CookieContainer = cookies;
+		request.ContentLength = formData.Length;
+
+		// You could add authentication here as well if needed:
+		// request.PreAuthenticate = true;
+		// request.AuthenticationLevel = System.Net.Security.AuthenticationLevel.MutualAuthRequested;
+		// request.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(System.Text.Encoding.Default.GetBytes("username" + ":" + "password")));
+
+		// Send the form data to the request.
+		using (Stream requestStream = request.GetRequestStream())
+		{
+			requestStream.Write(formData, 0, formData.Length);
+			requestStream.Close();
+		}
+
+		return request.GetResponse() as HttpWebResponse;
+	}
+
+	private static byte[] GetMultipartFormData(Dictionary<string, object> postParameters, string boundary)
+	{
+		Stream formDataStream = new System.IO.MemoryStream();
+		bool needsCLRF = false;
+
+		foreach (var param in postParameters)
+		{
+			// Thanks to feedback from commenters, add a CRLF to allow multiple parameters to be added.
+			// Skip it on the first parameter, add it to subsequent parameters.
+			if (needsCLRF)
+				formDataStream.Write(encoding.GetBytes("\r\n"), 0, encoding.GetByteCount("\r\n"));
+
+			needsCLRF = true;
+
+			if (param.Value is FileParameter)
+			{
+				FileParameter fileToUpload = (FileParameter)param.Value;
+
+				// Add just the first part of this param, since we will write the file data directly to the Stream
+				string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n",
+					boundary,
+					param.Key,
+					fileToUpload.FileName ?? param.Key,
+					fileToUpload.ContentType ?? "application/octet-stream");
+
+				formDataStream.Write(encoding.GetBytes(header), 0, encoding.GetByteCount(header));
+
+				// Write the file data directly to the Stream, rather than serializing it to a string.
+				formDataStream.Write(fileToUpload.File, 0, fileToUpload.File.Length);
+			}
+			else
+			{
+				string postData = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}",
+					boundary,
+					param.Key,
+					param.Value);
+				formDataStream.Write(encoding.GetBytes(postData), 0, encoding.GetByteCount(postData));
+			}
+		}
+
+		// Add the end of the request.  Start with a newline
+		string footer = "\r\n--" + boundary + "--\r\n";
+		formDataStream.Write(encoding.GetBytes(footer), 0, encoding.GetByteCount(footer));
+
+		// Dump the Stream into a byte[]
+		formDataStream.Position = 0;
+		byte[] formData = new byte[formDataStream.Length];
+		formDataStream.Read(formData, 0, formData.Length);
+		formDataStream.Close();
+
+		return formData;
+	}
+
+	public class FileParameter
+	{
+		public byte[] File { get; set; }
+		public string FileName { get; set; }
+		public string ContentType { get; set; }
+		public FileParameter(byte[] file) : this(file, null) { }
+		public FileParameter(byte[] file, string filename) : this(file, filename, null) { }
+		public FileParameter(byte[] file, string filename, string contenttype)
+		{
+			File = file;
+			FileName = filename;
+			ContentType = contenttype;
+		}
+	}
+}

--- /dev/null
+++ b/KerbalStuff/KerbalStuff.cs
@@ -1,1 +1,207 @@
-
+using MiniJSON;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+
+namespace KerbalStuff
+{
+	public class KerbalStuff : KerbalStuffReadOnly
+	{
+		public static Dictionary<string, object> Login(string username, string password)
+		{
+			string uri = KerbalStuffAction.Login.UriFormat;
+
+			Dictionary<string, object> postParams = new Dictionary<string, object>();
+			postParams.Add("username", username);
+			postParams.Add("password", password);
+
+			ExecutePostRequest(uri, postParams, null);
+
+			if (currentResponse.Cookies.Count > 0)
+			{
+				cookies = currentResponse.Cookies;
+			}
+
+			if (currentJson != null && currentJson is Dictionary<string, object>)
+			{
+				return currentJson as Dictionary<string, object>;
+			}
+
+			return null;
+		}
+
+		public static Dictionary<string, object> Create(Mod mod, string fileName, string filePath)
+		{
+			if (mod == null)
+			{
+				throw new ArgumentNullException("KerbalStuffWrapper.Create: mod argument cannot be null.");
+			}
+			else
+			{
+				if (mod.name == string.Empty)
+					throw new ArgumentException("mod.name cannot be empty.");
+				if (mod.license == string.Empty)
+					throw new ArgumentException("mod.license cannot be empty.");
+				if (mod.short_description == string.Empty)
+					throw new ArgumentException("mod.short_description cannot be empty.");
+				if (mod.versions.Count < 1)
+					throw new ArgumentException("mod must have a single version to create.");
+				else if (mod.versions[0] == null)
+				{
+					throw new ArgumentNullException("mod.versions[0] cannot be null.");
+				}
+				else
+				{
+					if (mod.versions[0].friendly_version == string.Empty)
+						throw new ArgumentException("mod.versions[0].friendly_version cannot be empty.");
+					if (mod.versions[0].ksp_version == string.Empty)
+						throw new ArgumentException("mod.versions[0].ksp_version cannot be empty.");
+				}
+			}
+
+			if (cookies == null)
+			{
+				throw new Exception("KerbalStuffWrapper.Create: Must log in first.");
+			}
+
+			if (!File.Exists(filePath))
+			{
+				throw new IOException(string.Format("KerbalStuffWrapper.Create: File '{0}' does not exist.", filePath));
+			}
+
+			Dictionary<string, object> postParams = new Dictionary<string, object>();
+			postParams.Add("name", mod.name);
+			postParams.Add("short-description", mod.short_description);
+			postParams.Add("license", mod.license);
+			postParams.Add("version", mod.versions[0].friendly_version);
+			postParams.Add("ksp-version", mod.versions[0].ksp_version);
+			postParams.Add("zipball", ReadZipballParameter(fileName, filePath));
+
+			ExecutePostRequest(KerbalStuffAction.Create.UriFormat, postParams, cookies);
+
+			if (currentJson != null && currentJson is Dictionary<string, object>)
+			{
+				var rval = currentJson as Dictionary<string, object>;
+
+				if (rval.ContainsKey("reason"))
+				{
+					rval["message"] = rval["reason"];
+				}
+
+				return rval;
+			}
+
+			return null;
+		}
+
+		public static Dictionary<string, object> Update(long modId, ModVersion version, bool notifyFollowers, string fileName, string filePath)
+		{
+			if (version == null)
+			{
+				throw new ArgumentNullException("KerbalStuffWrapper.Update: version cannot be null");
+			}
+			if (version.friendly_version == string.Empty)
+				throw new ArgumentException("KerbalStuffWrapper.Update: version.friendly_version cannot be empty");
+			if (version.ksp_version == string.Empty)
+				throw new ArgumentException("KerbalStuffWrapper.Update: version.ksp_version cannot be empty");
+
+			if (cookies == null)
+			{
+				throw new Exception("KerbalStuffWrapper.Update: Must log in first.");
+			}
+
+			if (!File.Exists(filePath))
+			{
+				throw new IOException(string.Format("KerbalStuffWrapper.Update: File '{0}' does not exist.", filePath));
+			}
+
+			string uri = string.Format(KerbalStuffAction.Update.UriFormat, modId);
+
+			Dictionary<string, object> postParams = new Dictionary<string, object>();
+			postParams.Add("version", version.friendly_version);
+			postParams.Add("ksp-version", version.ksp_version);
+
+			if (version.changelog != null && version.changelog != string.Empty)
+			{
+				postParams.Add("changelog", version.changelog);
+			}
+
+			postParams.Add("notify-followers", notifyFollowers ? "yes" : "no");
+
+			postParams.Add("zipball", ReadZipballParameter(fileName, filePath));
+
+			ExecutePostRequest(uri, postParams, cookies);
+
+			if (currentJson != null && currentJson is Dictionary<string, object>)
+			{
+				return currentJson as Dictionary<string, object>;
+			}
+
+			return null;
+		}
+
+		protected static void ExecutePostRequest(string uri, Dictionary<string, object> postParams, CookieCollection cookieCollection = null)
+		{
+			currentJson = null;
+			currentRequest = null;
+			currentResponse = null;
+
+			CookieContainer jar = new CookieContainer();
+
+			if (cookieCollection != null)
+			{
+				jar.Add(cookieCollection);
+			}
+
+			try
+			{
+				currentResponse = FormUpload.MultipartFormDataPost(
+					uri,
+					"KerbalStuffWrapper by toadicus",
+					postParams,
+					jar
+				);
+			}
+			catch (WebException ex)
+			{
+				currentResponse = ex.Response as HttpWebResponse;
+			}
+
+			if (currentResponse.ContentType == "application/json")
+			{
+				var responseReader = new StreamReader(currentResponse.GetResponseStream());
+
+				string json = responseReader.ReadToEnd();
+
+				currentJson = Json.Deserialize(json);
+			}
+		}
+
+		protected static FormUpload.FileParameter ReadZipballParameter(string fileName, string filePath)
+		{
+			using (FileStream file = File.OpenRead(filePath))
+			{
+				byte[] buffer = new byte[1 << 16];
+				int bytesRead;
+
+				MemoryStream stream = new MemoryStream();
+
+				while ((bytesRead = file.Read(buffer, 0, buffer.Length)) > 0)
+				{
+					stream.Write(buffer, 0, bytesRead);
+				}
+
+				byte[] fileBytes = stream.GetBuffer();
+
+				return new FormUpload.FileParameter(fileBytes, fileName, "application/zip");
+			}
+		}
+
+		protected KerbalStuff() {}
+	}
+}
+
+

--- /dev/null
+++ b/KerbalStuff/KerbalStuff.csproj
@@ -1,1 +1,49 @@
-
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
+    <ProductVersion>8.0.30703</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{0CA12F64-AD3B-41E6-8A35-8781A8DAD1C1}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <RootNamespace>KerbalStuff</RootNamespace>
+    <AssemblyName>KerbalStuff</AssemblyName>
+    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug</OutputPath>
+    <DefineConstants>DEBUG;</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+    <PlatformTarget>x86</PlatformTarget>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
+    <DebugType>full</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+    <PlatformTarget>x86</PlatformTarget>
+  </PropertyGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <ItemGroup>
+    <ProjectReference Include="..\KerbalStuffReadOnly\KerbalStuffReadOnly.csproj">
+      <Project>{720FA70F-D785-48ED-BA45-561921E0EEEC}</Project>
+      <Name>KerbalStuffReadOnly</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Net" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="KerbalStuff.cs" />
+    <Compile Include="FormUpload.cs" />
+  </ItemGroup>
+</Project>

--- /dev/null
+++ b/KerbalStuffReadOnly/KerbalStuffReadOnly.cs
@@ -1,1 +1,223 @@
-
+using MiniJSON;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+
+namespace KerbalStuff
+{
+	public class KerbalStuffReadOnly
+	{
+		public const string RootUri = "https://kerbalstuff.com";
+
+		public const string APIUri = RootUri + "/api";
+
+		public const string UserAgent = "KerbalStuffWrapper by toadicus";
+
+		public static Mod ModInfo(long modId)
+		{
+			string uri = string.Format(KerbalStuffAction.ModInfo.UriFormat, modId);
+
+			ExecuteGetRequest(uri, KerbalStuffAction.ModInfo.RequestMethod);
+
+			Mod mod = null;
+
+			if (currentJson != null && currentJson is Dictionary<string, object>)
+			{
+				mod = new Mod(currentJson as Dictionary<string, object>);
+			}
+
+			return mod;
+		}
+
+		public static ModVersion ModLatest(long modId)
+		{
+			string uri = string.Format(KerbalStuffAction.ModLatest.UriFormat, modId);
+
+			ExecuteGetRequest(uri, KerbalStuffAction.ModLatest.RequestMethod);
+
+			ModVersion ver = null;
+
+			if (currentJson != null && currentJson is Dictionary<string, object>)
+			{
+				ver = new ModVersion(currentJson as Dictionary<string, object>);
+			}
+
+			return ver;
+		}
+
+		public static List<Mod> ModSearch(string query)
+		{
+			string uri = string.Format(KerbalStuffAction.ModSearch.UriFormat, query);
+
+			ExecuteGetRequest(uri, KerbalStuffAction.ModSearch.RequestMethod);
+
+			List<Mod> rList = new List<Mod>();
+
+			if (currentJson != null && currentJson is List<object>)
+			{
+				foreach (var modObj in (currentJson as List<object>))
+				{
+					if (modObj is Dictionary<string, object>)
+					{
+						rList.Add(new Mod(modObj as Dictionary<string, object>));
+					}
+				}
+			}
+
+			return rList;
+		}
+
+		public static User UserInfo(string username)
+		{
+			ExecuteGetRequest(KerbalStuffAction.UserInfo, false, username);
+
+
+			User user = null;
+
+			if (currentJson != null && currentJson is Dictionary<string, object>)
+			{
+				user = new User(currentJson);
+			}
+
+			return user;
+		}
+
+		public static List<User> UserSearch(string query)
+		{
+			ExecuteGetRequest(KerbalStuffAction.UserSearch, false, query);
+
+			List<User> users = new List<User>();
+
+			if (currentJson != null && currentJson is List<object>)
+			{
+				foreach (object userObj in (currentJson as List<object>))
+				{
+					users.Add(new User(userObj));
+				}
+			}
+
+			return users;
+		}
+
+		public static HttpWebResponse currentResponse
+		{
+			get;
+			protected set;
+		}
+
+		public static CookieCollection cookies
+		{
+			get;
+			protected set;
+		}
+
+		protected static HttpWebRequest currentRequest;
+
+		public static object currentJson
+		{
+			get;
+			protected set;
+		}
+
+		protected static void ExecuteGetRequest(KerbalStuffAction action, bool assignCookies, params object[] formatArgs)
+		{
+			string uri = string.Format(action.UriFormat, formatArgs);
+
+			ExecuteGetRequest(uri, action.RequestMethod);
+		}
+
+		protected static void ExecuteGetRequest(string uri, string method)
+		{
+			currentJson = null;
+			currentRequest = null;
+			currentResponse = null;
+
+			if (uri == string.Empty)
+			{
+				throw new ArgumentOutOfRangeException("KerbalStuffWrapper.ExecuteRequest: uri must not be empty.");
+			}
+
+			uri = Uri.EscapeUriString(uri);
+
+			method = method.ToUpper();
+
+			if (method != "POST" && method != "GET")
+			{
+				throw new ArgumentOutOfRangeException("KerbalStuffWrapper.ExecuteRequest: method must be POST or GET.");
+			}
+
+			currentRequest = (HttpWebRequest)WebRequest.Create(uri);
+			currentRequest.Method = method;
+
+			try
+			{
+				currentResponse = currentRequest.GetResponse() as HttpWebResponse;
+			}
+			catch (WebException ex)
+			{
+				currentResponse = ex.Response as HttpWebResponse;
+			}
+
+			if (currentResponse.ContentType == "application/json")
+			{
+				var responseReader = new StreamReader(currentResponse.GetResponseStream());
+
+				string json = responseReader.ReadToEnd();
+
+				currentJson = Json.Deserialize(json);
+			}
+		}
+
+		protected KerbalStuffReadOnly() {}
+	}
+
+	public struct KerbalStuffAction
+	{
+		public static readonly KerbalStuffAction Create = new KerbalStuffAction("create", "/mod/create", "POST");
+		public static readonly KerbalStuffAction Login = new KerbalStuffAction("login", "/login", "POST");
+		public static readonly KerbalStuffAction ModInfo = new KerbalStuffAction("modinfo", "/mod/{0:d}", "GET");
+		public static readonly KerbalStuffAction ModLatest = new KerbalStuffAction(
+			"modlatest",
+			"/mod/{0:d}/latest",
+			"GET"
+		);
+		public static readonly KerbalStuffAction ModSearch = new KerbalStuffAction(
+			"modsearch",
+			"/search/mod?query={0}",
+			"GET"
+		);
+		public static readonly KerbalStuffAction Update = new KerbalStuffAction("update", "/mod/{0:d}/update", "POST");
+		public static readonly KerbalStuffAction UserInfo = new KerbalStuffAction("userinfo", "/user/{0}", "GET");
+		public static readonly KerbalStuffAction UserSearch = new KerbalStuffAction(
+			"usersearch",
+			"/search/user?query={0}",
+			"GET"
+		);
+
+		public string Action;
+
+		public string UriPathFormat;
+
+		public string RequestMethod;
+
+		public string UriFormat
+		{
+			get
+			{
+				return string.Format("{0}{1}", KerbalStuffReadOnly.APIUri, this.UriPathFormat);
+			}
+		}
+
+		public KerbalStuffAction(string action, string uriFormat, string requestMethod) : this()
+		{
+			this.Action = action;
+			this.UriPathFormat = uriFormat;
+			this.RequestMethod = requestMethod;
+		}
+	}
+}
+
+

--- /dev/null
+++ b/KerbalStuffReadOnly/KerbalStuffReadOnly.csproj
@@ -1,1 +1,45 @@
-
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
+    <ProductVersion>8.0.30703</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{720FA70F-D785-48ED-BA45-561921E0EEEC}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <RootNamespace>KerbalStuff</RootNamespace>
+    <AssemblyName>KerbalStuffReadOnly</AssemblyName>
+    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug</OutputPath>
+    <DefineConstants>DEBUG;</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+    <PlatformTarget>x86</PlatformTarget>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
+    <DebugType>full</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+    <PlatformTarget>x86</PlatformTarget>
+  </PropertyGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Net" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Mod.cs" />
+    <Compile Include="User.cs" />
+    <Compile Include="MiniJSON.cs" />
+    <Compile Include="KerbalStuffReadOnly.cs" />
+  </ItemGroup>
+</Project>

--- /dev/null
+++ b/KerbalStuffReadOnly/MiniJSON.cs
@@ -1,1 +1,548 @@
-
+/*
+ * Copyright (c) 2013 Calvin Rien
+ *
+ * Based on the JSON parser by Patrick van Bergen
+ * http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html
+ *
+ * Simplified it so that it doesn't throw exceptions
+ * and can be used in Unity iPhone with maximum code stripping.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace MiniJSON {
+	// Example usage:
+	//
+	//  using UnityEngine;
+	//  using System.Collections;
+	//  using System.Collections.Generic;
+	//  using MiniJSON;
+	//
+	//  public class MiniJSONTest : MonoBehaviour {
+	//      void Start () {
+	//          var jsonString = "{ \"array\": [1.44,2,3], " +
+	//                          "\"object\": {\"key1\":\"value1\", \"key2\":256}, " +
+	//                          "\"string\": \"The quick brown fox \\\"jumps\\\" over the lazy dog \", " +
+	//                          "\"unicode\": \"\\u3041 Men\u00fa sesi\u00f3n\", " +
+	//                          "\"int\": 65536, " +
+	//                          "\"float\": 3.1415926, " +
+	//                          "\"bool\": true, " +
+	//                          "\"null\": null }";
+	//
+	//          var dict = Json.Deserialize(jsonString) as Dictionary<string,object>;
+	//
+	//          Debug.Log("deserialized: " + dict.GetType());
+	//          Debug.Log("dict['array'][0]: " + ((List<object>) dict["array"])[0]);
+	//          Debug.Log("dict['string']: " + (string) dict["string"]);
+	//          Debug.Log("dict['float']: " + (double) dict["float"]); // floats come out as doubles
+	//          Debug.Log("dict['int']: " + (long) dict["int"]); // ints come out as longs
+	//          Debug.Log("dict['unicode']: " + (string) dict["unicode"]);
+	//
+	//          var str = Json.Serialize(dict);
+	//
+	//          Debug.Log("serialized: " + str);
+	//      }
+	//  }
+
+	/// <summary>
+	/// This class encodes and decodes JSON strings.
+	/// Spec. details, see http://www.json.org/
+	///
+	/// JSON uses Arrays and Objects. These correspond here to the datatypes IList and IDictionary.
+	/// All numbers are parsed to doubles.
+	/// </summary>
+	public static class Json {
+		/// <summary>
+		/// Parses the string json into a value
+		/// </summary>
+		/// <param name="json">A JSON string.</param>
+		/// <returns>An List&lt;object&gt;, a Dictionary&lt;string, object&gt;, a double, an integer,a string, null, true, or false</returns>
+		public static object Deserialize(string json) {
+			// save the string for debug information
+			if (json == null) {
+				return null;
+			}
+
+			return Parser.Parse(json);
+		}
+
+		sealed class Parser : IDisposable {
+			const string WORD_BREAK = "{}[],:\"";
+
+			public static bool IsWordBreak(char c) {
+				return Char.IsWhiteSpace(c) || WORD_BREAK.IndexOf(c) != -1;
+			}
+
+			enum TOKEN {
+				NONE,
+				CURLY_OPEN,
+				CURLY_CLOSE,
+				SQUARED_OPEN,
+				SQUARED_CLOSE,
+				COLON,
+				COMMA,
+				STRING,
+				NUMBER,
+				TRUE,
+				FALSE,
+				NULL
+			};
+
+			StringReader json;
+
+			Parser(string jsonString) {
+				json = new StringReader(jsonString);
+			}
+
+			public static object Parse(string jsonString) {
+				using (var instance = new Parser(jsonString)) {
+					return instance.ParseValue();
+				}
+			}
+
+			public void Dispose() {
+				json.Dispose();
+				json = null;
+			}
+
+			Dictionary<string, object> ParseObject() {
+				Dictionary<string, object> table = new Dictionary<string, object>();
+
+				// ditch opening brace
+				json.Read();
+
+				// {
+				while (true) {
+					switch (NextToken) {
+						case TOKEN.NONE:
+							return null;
+						case TOKEN.COMMA:
+							continue;
+						case TOKEN.CURLY_CLOSE:
+							return table;
+						default:
+							// name
+							string name = ParseString();
+							if (name == null) {
+								return null;
+							}
+
+							// :
+							if (NextToken != TOKEN.COLON) {
+								return null;
+							}
+							// ditch the colon
+							json.Read();
+
+							// value
+							table[name] = ParseValue();
+							break;
+					}
+				}
+			}
+
+			List<object> ParseArray() {
+				List<object> array = new List<object>();
+
+				// ditch opening bracket
+				json.Read();
+
+				// [
+				var parsing = true;
+				while (parsing) {
+					TOKEN nextToken = NextToken;
+
+					switch (nextToken) {
+						case TOKEN.NONE:
+							return null;
+						case TOKEN.COMMA:
+							continue;
+						case TOKEN.SQUARED_CLOSE:
+							parsing = false;
+							break;
+						default:
+							object value = ParseByToken(nextToken);
+
+							array.Add(value);
+							break;
+					}
+				}
+
+				return array;
+			}
+
+			object ParseValue() {
+				TOKEN nextToken = NextToken;
+				return ParseByToken(nextToken);
+			}
+
+			object ParseByToken(TOKEN token) {
+				switch (token) {
+					case TOKEN.STRING:
+						return ParseString();
+					case TOKEN.NUMBER:
+						return ParseNumber();
+					case TOKEN.CURLY_OPEN:
+						return ParseObject();
+					case TOKEN.SQUARED_OPEN:
+						return ParseArray();
+					case TOKEN.TRUE:
+						return true;
+					case TOKEN.FALSE:
+						return false;
+					case TOKEN.NULL:
+						return null;
+					default:
+						return null;
+				}
+			}
+
+			string ParseString() {
+				StringBuilder s = new StringBuilder();
+				char c;
+
+				// ditch opening quote
+				json.Read();
+
+				bool parsing = true;
+				while (parsing) {
+
+					if (json.Peek() == -1) {
+						parsing = false;
+						break;
+					}
+
+					c = NextChar;
+					switch (c) {
+						case '"':
+							parsing = false;
+							break;
+						case '\\':
+							if (json.Peek() == -1) {
+								parsing = false;
+								break;
+							}
+
+							c = NextChar;
+							switch (c) {
+								case '"':
+								case '\\':
+								case '/':
+									s.Append(c);
+									break;
+								case 'b':
+									s.Append('\b');
+									break;
+								case 'f':
+									s.Append('\f');
+									break;
+								case 'n':
+									s.Append('\n');
+									break;
+								case 'r':
+									s.Append('\r');
+									break;
+								case 't':
+									s.Append('\t');
+									break;
+								case 'u':
+									var hex = new char[4];
+
+									for (int i=0; i< 4; i++) {
+										hex[i] = NextChar;
+									}
+
+									s.Append((char) Convert.ToInt32(new string(hex), 16));
+									break;
+							}
+							break;
+						default:
+							s.Append(c);
+							break;
+					}
+				}
+
+				return s.ToString();
+			}
+
+			object ParseNumber() {
+				string number = NextWord;
+
+				if (number.IndexOf('.') == -1) {
+					long parsedInt;
+					Int64.TryParse(number, out parsedInt);
+					return parsedInt;
+				}
+
+				double parsedDouble;
+				Double.TryParse(number, out parsedDouble);
+				return parsedDouble;
+			}
+
+			void EatWhitespace() {
+				while (Char.IsWhiteSpace(PeekChar)) {
+					json.Read();
+
+					if (json.Peek() == -1) {
+						break;
+					}
+				}
+			}
+
+			char PeekChar {
+				get {
+					return Convert.ToChar(json.Peek());
+				}
+			}
+
+			char NextChar {
+				get {
+					return Convert.ToChar(json.Read());
+				}
+			}
+
+			string NextWord {
+				get {
+					StringBuilder word = new StringBuilder();
+
+					while (!IsWordBreak(PeekChar)) {
+						word.Append(NextChar);
+
+						if (json.Peek() == -1) {
+							break;
+						}
+					}
+
+					return word.ToString();
+				}
+			}
+
+			TOKEN NextToken {
+				get {
+					EatWhitespace();
+
+					if (json.Peek() == -1) {
+						return TOKEN.NONE;
+					}
+
+					switch (PeekChar) {
+						case '{':
+							return TOKEN.CURLY_OPEN;
+						case '}':
+							json.Read();
+							return TOKEN.CURLY_CLOSE;
+						case '[':
+							return TOKEN.SQUARED_OPEN;
+						case ']':
+							json.Read();
+							return TOKEN.SQUARED_CLOSE;
+						case ',':
+							json.Read();
+							return TOKEN.COMMA;
+						case '"':
+							return TOKEN.STRING;
+						case ':':
+							return TOKEN.COLON;
+						case '0':
+						case '1':
+						case '2':
+						case '3':
+						case '4':
+						case '5':
+						case '6':
+						case '7':
+						case '8':
+						case '9':
+						case '-':
+							return TOKEN.NUMBER;
+					}
+
+					switch (NextWord) {
+						case "false":
+							return TOKEN.FALSE;
+						case "true":
+							return TOKEN.TRUE;
+						case "null":
+							return TOKEN.NULL;
+					}
+
+					return TOKEN.NONE;
+				}
+			}
+		}
+
+		/// <summary>
+		/// Converts a IDictionary / IList object or a simple type (string, int, etc.) into a JSON string
+		/// </summary>
+		/// <param name="json">A Dictionary&lt;string, object&gt; / List&lt;object&gt;</param>
+		/// <returns>A JSON encoded string, or null if object 'json' is not serializable</returns>
+		public static string Serialize(object obj) {
+			return Serializer.Serialize(obj);
+		}
+
+		sealed class Serializer {
+			StringBuilder builder;
+
+			Serializer() {
+				builder = new StringBuilder();
+			}
+
+			public static string Serialize(object obj) {
+				var instance = new Serializer();
+
+				instance.SerializeValue(obj);
+
+				return instance.builder.ToString();
+			}
+
+			void SerializeValue(object value) {
+				IList asList;
+				IDictionary asDict;
+				string asStr;
+
+				if (value == null) {
+					builder.Append("null");
+				} else if ((asStr = value as string) != null) {
+					SerializeString(asStr);
+				} else if (value is bool) {
+					builder.Append((bool) value ? "true" : "false");
+				} else if ((asList = value as IList) != null) {
+					SerializeArray(asList);
+				} else if ((asDict = value as IDictionary) != null) {
+					SerializeObject(asDict);
+				} else if (value is char) {
+					SerializeString(new string((char) value, 1));
+				} else {
+					SerializeOther(value);
+				}
+			}
+
+			void SerializeObject(IDictionary obj) {
+				bool first = true;
+
+				builder.Append('{');
+
+				foreach (object e in obj.Keys) {
+					if (!first) {
+						builder.Append(',');
+					}
+
+					SerializeString(e.ToString());
+					builder.Append(':');
+
+					SerializeValue(obj[e]);
+
+					first = false;
+				}
+
+				builder.Append('}');
+			}
+
+			void SerializeArray(IList anArray) {
+				builder.Append('[');
+
+				bool first = true;
+
+				foreach (object obj in anArray) {
+					if (!first) {
+						builder.Append(',');
+					}
+
+					SerializeValue(obj);
+
+					first = false;
+				}
+
+				builder.Append(']');
+			}
+
+			void SerializeString(string str) {
+				builder.Append('\"');
+
+				char[] charArray = str.ToCharArray();
+				foreach (var c in charArray) {
+					switch (c) {
+						case '"':
+							builder.Append("\\\"");
+							break;
+						case '\\':
+							builder.Append("\\\\");
+							break;
+						case '\b':
+							builder.Append("\\b");
+							break;
+						case '\f':
+							builder.Append("\\f");
+							break;
+						case '\n':
+							builder.Append("\\n");
+							break;
+						case '\r':
+							builder.Append("\\r");
+							break;
+						case '\t':
+							builder.Append("\\t");
+							break;
+						default:
+							int codepoint = Convert.ToInt32(c);
+							if ((codepoint >= 32) && (codepoint <= 126)) {
+								builder.Append(c);
+							} else {
+								builder.Append("\\u");
+								builder.Append(codepoint.ToString("x4"));
+							}
+							break;
+					}
+				}
+
+				builder.Append('\"');
+			}
+
+			void SerializeOther(object value) {
+				// NOTE: decimals lose precision during serialization.
+				// They always have, I'm just letting you know.
+				// Previously floats and doubles lost precision too.
+				if (value is float) {
+					builder.Append(((float) value).ToString("R"));
+				} else if (value is int
+					|| value is uint
+					|| value is long
+					|| value is sbyte
+					|| value is byte
+					|| value is short
+					|| value is ushort
+					|| value is ulong) {
+					builder.Append(value);
+				} else if (value is double
+					|| value is decimal) {
+					builder.Append(Convert.ToDouble(value).ToString("R"));
+				} else {
+					SerializeString(value.ToString());
+				}
+			}
+		}
+	}
+}
+

--- /dev/null
+++ b/KerbalStuffReadOnly/Mod.cs
@@ -1,1 +1,166 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
 
+namespace KerbalStuff
+{
+	public class Mod
+	{
+		public long downloads
+		{
+			get;
+			private set;
+		}
+
+		public string name
+		{
+			get;
+			private set;
+		}
+
+		public long followers
+		{
+			get;
+			private set;
+		}
+
+		public string author
+		{
+			get;
+			private set;
+		}
+
+		public long default_version_id
+		{
+			get;
+			private set;
+		}
+
+		public List<ModVersion> versions
+		{
+			get;
+			private set;
+		}
+
+		public long id
+		{
+			get;
+			private set;
+		}
+
+		public string short_description
+		{
+			get;
+			private set;
+		}
+
+		public string license
+		{
+			get;
+			private set;
+		}
+
+		public Mod(Dictionary<string, object> jsonDict) : this()
+		{
+			this.downloads = (long)jsonDict["downloads"];
+			this.name = (string)jsonDict["name"];
+			this.followers = (long)jsonDict["followers"];
+			this.author = (string)jsonDict["author"];
+			this.default_version_id = (long)jsonDict["default_version_id"];
+			this.id = (long)jsonDict["id"];
+			this.short_description = (string)jsonDict["short_description"];
+
+			if (jsonDict.ContainsKey("versions"))
+			{
+				foreach (var ver in (jsonDict["versions"] as List<object>))
+				{
+					if (ver is Dictionary<string, object>)
+					{
+						this.versions.Add(new ModVersion(ver as Dictionary<string, object>));
+					}
+				}
+			}
+		}
+
+		public Mod(string name, string short_description, string version, string ksp_version, string license) : this()
+		{
+			this.name = name;
+			this.short_description = short_description;
+			this.license = license;
+
+			this.versions.Add(new ModVersion(version, ksp_version));
+		}
+
+		private Mod()
+		{
+			this.versions = new List<ModVersion>();
+		}
+
+		public override string ToString()
+		{
+			return string.Format("Mod: {1}\nid: {6}\nauthor: {3}\ndownloads: {0}\nfollowers: {2}\nshort_description: {7}\ndefault_version_id: {4}\nversions:\n[\n{5}\n]\n", downloads, name, followers, author, default_version_id, string.Join("\n", versions.Select(v => v.ToString()).ToArray()), id, short_description);
+		}
+	}
+
+	public class ModVersion
+	{
+		public string changelog
+		{
+			get;
+			private set;
+		}
+
+		public string ksp_version
+		{
+			get;
+			private set;
+		}
+
+		public string download_path
+		{
+			get;
+			private set;
+		}
+
+		public long id
+		{
+			get;
+			private set;
+		}
+
+		public string friendly_version
+		{
+			get;
+			private set;
+		}
+
+		public ModVersion(Dictionary<string, object> jsonDict) : this()
+		{
+			this.changelog = (string)jsonDict["changelog"];
+			this.ksp_version = (string)jsonDict["ksp_version"];
+			this.download_path = (string)jsonDict["download_path"];
+			this.id = (long)jsonDict["id"];
+			this.friendly_version = (string)jsonDict["friendly_version"];
+		}
+
+		public ModVersion(string version, string ksp_version, string changelog) : this(version, ksp_version)
+		{
+			this.changelog = changelog;
+		}
+
+		public ModVersion(string version, string ksp_version) : this()
+		{
+			this.friendly_version = version;
+			this.ksp_version = ksp_version;
+		}
+
+		private ModVersion() {}
+
+		public override string ToString()
+		{
+			return string.Format("ModVersion {4}:\nid: {3}\nksp_version: {1}\ndownload_path: {2}\nchangelog: {0}", changelog, ksp_version, download_path, id, friendly_version);
+		}
+	}
+}
+
+

--- /dev/null
+++ b/KerbalStuffReadOnly/User.cs
@@ -1,1 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
 
+namespace KerbalStuff
+{
+	public class User
+	{
+		public string username
+		{
+			get;
+			private set;
+		}
+
+		public string twitterUsername
+		{
+			get;
+			private set;
+		}
+
+		public List<Mod> mods
+		{
+			get;
+			private set;
+		}
+
+		public string redditUsername
+		{
+			get;
+			private set;
+		}
+
+		public string ircNick
+		{
+			get;
+			private set;
+		}
+
+		public string description
+		{
+			get;
+			private set;
+		}
+
+		public string forumUsername
+		{
+			get;
+			private set;
+		}
+
+		public User(Dictionary<string, object> jsonDict) : this()
+		{
+			this.username = (string)jsonDict["username"];
+			this.twitterUsername = (string)jsonDict["twitterUsername"];
+			this.redditUsername = (string)jsonDict["redditUsername"];
+			this.ircNick = (string)jsonDict["ircNick"];
+			this.forumUsername = (string)jsonDict["forumUsername"];
+
+			this.description = (string)jsonDict["description"];
+
+			this.mods = new List<Mod>();
+
+			foreach (object modObj in (jsonDict["mods"] as List<object>))
+			{
+				this.mods.Add(new Mod(modObj as Dictionary<string, object>));
+			}
+		}
+
+		public User(object jsonObj) : this((Dictionary<string, object>)jsonObj) {}
+
+		private User() {}
+
+		public override string ToString()
+		{
+			return string.Format(
+				"User: username={0}, twitterUsername={1}, redditUsername={3}, ircNick={4}, description={5}, forumUsername={6}\nmods:\n{2}",
+				username,
+				twitterUsername,
+				string.Join(
+					"\n",
+					mods.Select(m => m.ToString()).ToArray()
+				),
+				redditUsername,
+				ircNick,
+				description,
+				forumUsername
+			);
+		}
+	}
+}
+
+

--- a/KerbalStuffWrapper.sln
+++ b/KerbalStuffWrapper.sln
@@ -2,6 +2,10 @@
 Microsoft Visual Studio Solution File, Format Version 11.00
 # Visual Studio 2010
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KerbalStuffWrapper", "KerbalStuffWrapper\KerbalStuffWrapper.csproj", "{1E93CDA7-56A8-410F-A5A2-0ABB9210CA58}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KerbalStuffReadOnly", "KerbalStuffReadOnly\KerbalStuffReadOnly.csproj", "{720FA70F-D785-48ED-BA45-561921E0EEEC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KerbalStuff", "KerbalStuff\KerbalStuff.csproj", "{0CA12F64-AD3B-41E6-8A35-8781A8DAD1C1}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -9,10 +13,18 @@
 		Release|x86 = Release|x86
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{0CA12F64-AD3B-41E6-8A35-8781A8DAD1C1}.Debug|x86.ActiveCfg = Debug|x86
+		{0CA12F64-AD3B-41E6-8A35-8781A8DAD1C1}.Debug|x86.Build.0 = Debug|x86
+		{0CA12F64-AD3B-41E6-8A35-8781A8DAD1C1}.Release|x86.ActiveCfg = Release|x86
+		{0CA12F64-AD3B-41E6-8A35-8781A8DAD1C1}.Release|x86.Build.0 = Release|x86
 		{1E93CDA7-56A8-410F-A5A2-0ABB9210CA58}.Debug|x86.ActiveCfg = Debug|x86
 		{1E93CDA7-56A8-410F-A5A2-0ABB9210CA58}.Debug|x86.Build.0 = Debug|x86
 		{1E93CDA7-56A8-410F-A5A2-0ABB9210CA58}.Release|x86.ActiveCfg = Release|x86
 		{1E93CDA7-56A8-410F-A5A2-0ABB9210CA58}.Release|x86.Build.0 = Release|x86
+		{720FA70F-D785-48ED-BA45-561921E0EEEC}.Debug|x86.ActiveCfg = Debug|x86
+		{720FA70F-D785-48ED-BA45-561921E0EEEC}.Debug|x86.Build.0 = Debug|x86
+		{720FA70F-D785-48ED-BA45-561921E0EEEC}.Release|x86.ActiveCfg = Release|x86
+		{720FA70F-D785-48ED-BA45-561921E0EEEC}.Release|x86.Build.0 = Release|x86
 	EndGlobalSection
 	GlobalSection(MonoDevelopProperties) = preSolution
 		StartupItem = KerbalStuffWrapper\KerbalStuffWrapper.csproj

--- a/KerbalStuffWrapper/FormUpload.cs
+++ /dev/null
@@ -1,132 +1,1 @@
-// Implements multipart/form-data POST in C# http://www.ietf.org/rfc/rfc2388.txt
-// http://www.briangrinstead.com/blog/multipart-form-post-in-c
 
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Text;
-
-public static class FormUpload
-{
-	private static readonly Encoding encoding = Encoding.UTF8;
-
-	public static HttpWebResponse MultipartFormDataPost(string postUrl, string userAgent, Dictionary<string, object> postParameters)
-	{
-		return MultipartFormDataPost(postUrl, userAgent, postParameters, new CookieContainer());
-	}
-
-	public static HttpWebResponse MultipartFormDataPost(string postUrl, string userAgent, Dictionary<string, object> postParameters, CookieContainer cookies)
-	{
-		string formDataBoundary = String.Format("----------{0:N}", Guid.NewGuid());
-		string contentType = "multipart/form-data; boundary=" + formDataBoundary;
-
-		byte[] formData = GetMultipartFormData(postParameters, formDataBoundary);
-
-		return PostForm(postUrl, userAgent, contentType, formData, cookies);
-	}
-
-	private static HttpWebResponse PostForm(string postUrl, string userAgent, string contentType, byte[] formData)
-	{
-		return PostForm(postUrl, userAgent, contentType, formData, new CookieContainer());
-	}
-
-	private static HttpWebResponse PostForm(string postUrl, string userAgent, string contentType, byte[] formData, CookieContainer cookies)
-	{
-		HttpWebRequest request = WebRequest.Create(postUrl) as HttpWebRequest;
-
-		if (request == null)
-		{
-			throw new NullReferenceException("request is not a http request");
-		}
-
-		// Set up the request properties.
-		request.Method = "POST";
-		request.ContentType = contentType;
-		request.UserAgent = userAgent;
-		request.CookieContainer = cookies;
-		request.ContentLength = formData.Length;
-
-		// You could add authentication here as well if needed:
-		// request.PreAuthenticate = true;
-		// request.AuthenticationLevel = System.Net.Security.AuthenticationLevel.MutualAuthRequested;
-		// request.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(System.Text.Encoding.Default.GetBytes("username" + ":" + "password")));
-
-		// Send the form data to the request.
-		using (Stream requestStream = request.GetRequestStream())
-		{
-			requestStream.Write(formData, 0, formData.Length);
-			requestStream.Close();
-		}
-
-		return request.GetResponse() as HttpWebResponse;
-	}
-
-	private static byte[] GetMultipartFormData(Dictionary<string, object> postParameters, string boundary)
-	{
-		Stream formDataStream = new System.IO.MemoryStream();
-		bool needsCLRF = false;
-
-		foreach (var param in postParameters)
-		{
-			// Thanks to feedback from commenters, add a CRLF to allow multiple parameters to be added.
-			// Skip it on the first parameter, add it to subsequent parameters.
-			if (needsCLRF)
-				formDataStream.Write(encoding.GetBytes("\r\n"), 0, encoding.GetByteCount("\r\n"));
-
-			needsCLRF = true;
-
-			if (param.Value is FileParameter)
-			{
-				FileParameter fileToUpload = (FileParameter)param.Value;
-
-				// Add just the first part of this param, since we will write the file data directly to the Stream
-				string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n",
-					boundary,
-					param.Key,
-					fileToUpload.FileName ?? param.Key,
-					fileToUpload.ContentType ?? "application/octet-stream");
-
-				formDataStream.Write(encoding.GetBytes(header), 0, encoding.GetByteCount(header));
-
-				// Write the file data directly to the Stream, rather than serializing it to a string.
-				formDataStream.Write(fileToUpload.File, 0, fileToUpload.File.Length);
-			}
-			else
-			{
-				string postData = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}",
-					boundary,
-					param.Key,
-					param.Value);
-				formDataStream.Write(encoding.GetBytes(postData), 0, encoding.GetByteCount(postData));
-			}
-		}
-
-		// Add the end of the request.  Start with a newline
-		string footer = "\r\n--" + boundary + "--\r\n";
-		formDataStream.Write(encoding.GetBytes(footer), 0, encoding.GetByteCount(footer));
-
-		// Dump the Stream into a byte[]
-		formDataStream.Position = 0;
-		byte[] formData = new byte[formDataStream.Length];
-		formDataStream.Read(formData, 0, formData.Length);
-		formDataStream.Close();
-
-		return formData;
-	}
-
-	public class FileParameter
-	{
-		public byte[] File { get; set; }
-		public string FileName { get; set; }
-		public string ContentType { get; set; }
-		public FileParameter(byte[] file) : this(file, null) { }
-		public FileParameter(byte[] file, string filename) : this(file, filename, null) { }
-		public FileParameter(byte[] file, string filename, string contenttype)
-		{
-			File = file;
-			FileName = filename;
-			ContentType = contenttype;
-		}
-	}
-}

--- a/KerbalStuffWrapper/KerbalStuff.cs
+++ /dev/null
@@ -1,381 +1,1 @@
-using MiniJSON;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Text;
 
-// TODO: Robustify error-handling throughout.
-// TODO: Factor the upload stuff into a separate library.
-
-namespace KerbalStuff
-{
-	public static class KerbalStuff
-	{
-		public const string RootUri = "https://kerbalstuff.com";
-
-		public const string APIUri = RootUri + "/api";
-
-		public const string UserAgent = "KerbalStuffWrapper by toadicus";
-
-		// TODO: Find a more useful return value for Login.
-		// TODO: Convert Login to use ExecutePostRequest.
-		public static void Login(string username, string password)
-		{
-			Dictionary<string, object> postParams = new Dictionary<string, object>();
-			postParams.Add("username", username);
-			postParams.Add("password", password);
-
-			HttpWebResponse response = FormUpload.MultipartFormDataPost(
-				KerbalStuffAction.Login.UriFormat,
-				"KerbalStuffWrapper by toadicus",
-				postParams
-			);
-
-			cookies = response.Cookies;
-		}
-
-		// TODO: Find a more useful return value for Create.
-		public static string Create(Mod mod, string fileName, string filePath)
-		{
-			if (mod == null)
-			{
-				throw new ArgumentNullException("KerbalStuffWrapper.Create: mod argument cannot be null.");
-			}
-			else
-			{
-				if (mod.name == string.Empty)
-					throw new ArgumentException("mod.name cannot be empty.");
-				if (mod.license == string.Empty)
-					throw new ArgumentException("mod.license cannot be empty.");
-				if (mod.short_description == string.Empty)
-					throw new ArgumentException("mod.short_description cannot be empty.");
-				if (mod.versions.Count < 1)
-					throw new ArgumentException("mod must have a single version to create.");
-				else if (mod.versions[0] == null)
-				{
-					throw new ArgumentNullException("mod.versions[0] cannot be null.");
-				}
-				else
-				{
-					if (mod.versions[0].friendly_version == string.Empty)
-						throw new ArgumentException("mod.versions[0].friendly_version cannot be empty.");
-					if (mod.versions[0].ksp_version == string.Empty)
-						throw new ArgumentException("mod.versions[0].ksp_version cannot be empty.");
-				}
-			}
-
-			if (cookies == null)
-			{
-				throw new Exception("KerbalStuffWrapper.Create: Must log in first.");
-			}
-
-			if (!File.Exists(filePath))
-			{
-				throw new IOException(string.Format("KerbalStuffWrapper.Create: File '{0}' does not exist.", filePath));
-			}
-
-			Dictionary<string, object> postParams = new Dictionary<string, object>();
-			postParams.Add("name", mod.name);
-			postParams.Add("short-description", mod.short_description);
-			postParams.Add("license", mod.license);
-			postParams.Add("version", mod.versions[0].friendly_version);
-			postParams.Add("ksp-version", mod.versions[0].ksp_version);
-			postParams.Add("zipball", ReadZipballParameter(fileName, filePath));
-
-			ExecutePostRequest(KerbalStuffAction.Create.UriFormat, postParams, cookies);
-
-			return string.Concat(RootUri, (currentJson as Dictionary<string, object>)["url"]);
-		}
-
-		// TODO: Find a more useful return value for Update.
-		public static string Update(long modId, ModVersion version, bool notifyFollowers, string fileName, string filePath)
-		{
-			if (version == null)
-			{
-				throw new ArgumentNullException("KerbalStuffWrapper.Update: version cannot be null");
-			}
-			if (version.friendly_version == string.Empty)
-				throw new ArgumentException("KerbalStuffWrapper.Update: version.friendly_version cannot be empty");
-			if (version.ksp_version == string.Empty)
-				throw new ArgumentException("KerbalStuffWrapper.Update: version.ksp_version cannot be empty");
-
-			if (cookies == null)
-			{
-				throw new Exception("KerbalStuffWrapper.Update: Must log in first.");
-			}
-
-			if (!File.Exists(filePath))
-			{
-				throw new IOException(string.Format("KerbalStuffWrapper.Update: File '{0}' does not exist.", filePath));
-			}
-
-			string uri = string.Format(KerbalStuffAction.Update.UriFormat, modId);
-
-			Dictionary<string, object> postParams = new Dictionary<string, object>();
-			postParams.Add("version", version.friendly_version);
-			postParams.Add("ksp-version", version.ksp_version);
-
-			if (version.changelog != null && version.changelog != string.Empty)
-			{
-				postParams.Add("changelog", version.changelog);
-			}
-
-			postParams.Add("notify-followers", notifyFollowers ? "yes" : "no");
-
-			postParams.Add("zipball", ReadZipballParameter(fileName, filePath));
-
-			ExecutePostRequest(uri, postParams, cookies);
-
-			return string.Concat(RootUri, (currentJson as Dictionary<string, object>)["url"]);
-		}
-
-		public static Mod ModInfo(long modId)
-		{
-			string uri = string.Format(KerbalStuffAction.ModInfo.UriFormat, modId);
-
-			ExecuteGetRequest(uri, KerbalStuffAction.ModInfo.RequestMethod, false);
-
-			Mod mod = null;
-
-			if (currentJson != null && currentJson is Dictionary<string, object>)
-			{
-				mod = new Mod(currentJson as Dictionary<string, object>);
-			}
-
-			return mod;
-		}
-
-		public static ModVersion ModLatest(long modId)
-		{
-			string uri = string.Format(KerbalStuffAction.ModLatest.UriFormat, modId);
-
-			ExecuteGetRequest(uri, KerbalStuffAction.ModLatest.RequestMethod, false);
-
-			ModVersion ver = null;
-
-			if (currentJson != null && currentJson is Dictionary<string, object>)
-			{
-				ver = new ModVersion(currentJson as Dictionary<string, object>);
-			}
-
-			return ver;
-		}
-
-		public static List<Mod> ModSearch(string query)
-		{
-			string uri = string.Format(KerbalStuffAction.ModSearch.UriFormat, query);
-
-			ExecuteGetRequest(uri, KerbalStuffAction.ModSearch.RequestMethod, false);
-
-			List<Mod> rList = new List<Mod>();
-
-			if (currentJson != null && currentJson is List<object>)
-			{
-				foreach (var modObj in (currentJson as List<object>))
-				{
-					if (modObj is Dictionary<string, object>)
-					{
-						rList.Add(new Mod(modObj as Dictionary<string, object>));
-					}
-				}
-			}
-
-			return rList;
-		}
-
-		public static User UserInfo(string username)
-		{
-			ExecuteGetRequest(KerbalStuffAction.UserInfo, false, username);
-
-
-			User user = null;
-
-			if (currentJson != null && currentJson is Dictionary<string, object>)
-			{
-				user = new User(currentJson);
-			}
-
-			return user;
-		}
-
-		public static List<User> UserSearch(string query)
-		{
-			ExecuteGetRequest(KerbalStuffAction.UserSearch, false, query);
-
-			List<User> users = new List<User>();
-
-			if (currentJson != null && currentJson is List<object>)
-			{
-				foreach (object userObj in (currentJson as List<object>))
-				{
-					users.Add(new User(userObj));
-				}
-			}
-
-			return users;
-		}
-
-		private static HttpWebRequest currentRequest;
-		private static HttpWebResponse currentResponse;
-		private static CookieCollection cookies;
-
-		private static object currentJson;
-
-		private static void ExecuteGetRequest(KerbalStuffAction action, bool assignCookies, params object[] formatArgs)
-		{
-			string uri = string.Format(action.UriFormat, formatArgs);
-
-			ExecuteGetRequest(uri, action.RequestMethod, assignCookies);
-		}
-
-		private static void ExecuteGetRequest(string uri, string method, byte[] formData = null)
-		{
-			currentJson = null;
-			currentRequest = null;
-			currentResponse = null;
-
-			if (uri == string.Empty)
-			{
-				throw new ArgumentOutOfRangeException("KerbalStuffWrapper.ExecuteRequest: uri must not be empty.");
-			}
-
-			uri = Uri.EscapeUriString(uri);
-
-			method = method.ToUpper();
-
-			if (method != "POST" && method != "GET")
-			{
-				throw new ArgumentOutOfRangeException("KerbalStuffWrapper.ExecuteRequest: method must be POST or GET.");
-			}
-
-			currentRequest = (HttpWebRequest)WebRequest.Create(uri);
-			currentRequest.Method = method;
-
-			try
-			{
-				currentResponse = (HttpWebResponse)currentRequest.GetResponse();
-			}
-			catch (WebException ex)
-			{
-				currentResponse = ex.Response;
-				return false;
-			}
-
-			if (currentResponse.ContentType == "application/json")
-			{
-				var responseReader = new StreamReader(currentResponse.GetResponseStream());
-
-				string json = responseReader.ReadToEnd();
-
-				currentJson = Json.Deserialize(json);
-			}
-
-			return true;
-		}
-
-		private static bool ExecutePostRequest(string uri, Dictionary<string, object> postParams, CookieCollection cookieCollection = null)
-		{
-			currentJson = null;
-			currentRequest = null;
-			currentResponse = null;
-
-			CookieContainer jar = new CookieContainer();
-
-			if (cookieCollection != null)
-			{
-				jar.Add(cookieCollection);
-			}
-
-			try
-			{
-				currentResponse = FormUpload.MultipartFormDataPost(
-					uri,
-					/*"http://toad.homelinux.net/post_dump.php",*/
-					"KerbalStuffWrapper by toadicus",
-					postParams,
-					jar
-				);
-
-				currentJson = Json.Deserialize((new StreamReader(currentResponse.GetResponseStream())).ReadToEnd());
-			}
-			catch (WebException ex)
-			{
-				currentResponse = ex.Response;
-				return false;
-			}
-
-			return true;
-		}
-
-		private static FormUpload.FileParameter ReadZipballParameter(string fileName, string filePath)
-		{
-			using (FileStream file = File.OpenRead(filePath))
-			{
-				byte[] buffer = new byte[1 << 16];
-				int bytesRead;
-
-				MemoryStream stream = new MemoryStream();
-
-				while ((bytesRead = file.Read(buffer, 0, buffer.Length)) > 0)
-				{
-					stream.Write(buffer, 0, bytesRead);
-				}
-
-				byte[] fileBytes = stream.GetBuffer();
-
-				return new FormUpload.FileParameter(fileBytes, fileName, "application/zip");
-			}
-
-			return null;
-		}
-	}
-
-	public struct KerbalStuffAction
-	{
-		public static readonly KerbalStuffAction Create = new KerbalStuffAction("create", "/mod/create", "POST");
-		public static readonly KerbalStuffAction Login = new KerbalStuffAction("login", "/login", "POST");
-		public static readonly KerbalStuffAction ModInfo = new KerbalStuffAction("modinfo", "/mod/{0:d}", "GET");
-		public static readonly KerbalStuffAction ModLatest = new KerbalStuffAction(
-			"modlatest",
-			"/mod/{0:d}/latest",
-			"GET"
-		);
-		public static readonly KerbalStuffAction ModSearch = new KerbalStuffAction(
-			"modsearch",
-			"/search/mod?query={0}",
-			"GET"
-		);
-		public static readonly KerbalStuffAction Update = new KerbalStuffAction("update", "/mod/{0:d}/update", "POST");
-		public static readonly KerbalStuffAction UserInfo = new KerbalStuffAction("userinfo", "/user/{0}", "GET");
-		public static readonly KerbalStuffAction UserSearch = new KerbalStuffAction(
-			"usersearch",
-			"/search/user?query={0}",
-			"GET"
-		);
-
-		public string Action;
-
-		public string UriPathFormat;
-
-		public string RequestMethod;
-
-		public string UriFormat
-		{
-			get
-			{
-				return string.Format("{0}{1}", KerbalStuff.APIUri, this.UriPathFormat);
-			}
-		}
-
-		public KerbalStuffAction(string action, string uriFormat, string requestMethod) : this()
-		{
-			this.Action = action;
-			this.UriPathFormat = uriFormat;
-			this.RequestMethod = requestMethod;
-		}
-	}
-}
-
-

--- a/KerbalStuffWrapper/KerbalStuffWrapper.cs
+++ b/KerbalStuffWrapper/KerbalStuffWrapper.cs
@@ -1,93 +1,374 @@
-using MiniJSON;
+using CLAP;
+using CLAP.Interception;
+using CLAP.Validation;
 using System;
 using System.Collections.Generic;
 using System.IO;
-using System.Net;
-
-namespace KerbalStuffWrapper
+using System.Linq;
+
+// TODO: Write a console program that uses the KerbalStuff wrapper API.
+
+namespace KerbalStuff.Wrapper
 {
-	public static class KerbalStuffWrapper
+	public class KerbalStuffWrapper
 	{
-		private static HttpWebRequest currentRequest;
-		private static HttpWebResponse currentResponse;
-		private static CookieContainer cookies;
-
-		private static void ExecuteRequest(string uri, string method, bool assignCookies)
-		{
-			if (uri == string.Empty)
-			{
-				throw new ArgumentOutOfRangeException("KerbalStuffWrapper.ExecuteRequest: uri must not be empty.");
-			}
-
-			method = method.ToUpper();
-
-			if (method != "POST" && method != "GET")
-			{
-				throw new ArgumentOutOfRangeException("KerbalStuffWrapper.ExecuteRequest: method must be POST or GET.");
-			}
-
-			currentRequest = (HttpWebRequest)WebRequest.Create(uri);
-			currentRequest.Method = method;
-
-			if (assignCookies)
-			{
-				if (cookies == null)
-				{
-					throw new ArgumentNullException("KerbalStuffWrapper.ExecuteRequest: cookies must not be null.");
-				}
-
-				currentRequest.CookieContainer = cookies;
-			}
-
-
-		}
-
-		public static List<Dictionary<string, object>> Search(string query)
-		{
-			// ServicePointManager.ServerCertificateValidationCallback
-			string uri = string.Format(KerbalStuffAction.Search.UriFormat, query);
-
-			HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);
-			req.Method = KerbalStuffAction.Search.RequestMethod;
-
-			var responseReader = new StreamReader(req.GetResponse().GetResponseStream());
-
-			string json = responseReader.ReadToEnd();
-
-			Console.WriteLine(json);
-
-			object jsonObj = Json.Deserialize(json);
-
-			return (List<Dictionary<string, object>>)jsonObj;
-		}
-	}
-
-	public struct KerbalStuffAction
-	{
-		public static readonly KerbalStuffAction Search = new KerbalStuffAction("search", "search/mod?query=%s", "GET");
-		public static readonly KerbalStuffAction ModInfo = new KerbalStuffAction("modinfo", "mod/%d", "GET");
-
-		public const string APIUri = "https://www.kerbalstuff.com/api/";
-
-		public string Action;
-
-		public string UriPathFormat;
-
-		public string RequestMethod;
-
-		public string UriFormat
-		{
-			get
-			{
-				return string.Format("{0}{1}", APIUri, this.UriPathFormat);
-			}
-		}
-
-		public KerbalStuffAction(string action, string uriFormat, string requestMethod) : this()
-		{
-			this.Action = action;
-			this.UriPathFormat = uriFormat;
-			this.RequestMethod = requestMethod;
+		private static Parser<KerbalStuffWrapper> parser;
+		private static string[] args;
+
+		public static int Main(string[] args)
+		{
+			KerbalStuffWrapper.args = args;
+			parser = new Parser<KerbalStuffWrapper>();
+
+			return parser.RunStatic(args);
+		}
+
+		private static void Write(string message, TextWriter output)
+		{
+
+			output.Write(string.Format("{0}: {1}", appName, message));
+		}
+
+		private static void WriteOut(string message)
+		{
+			Write(message, Console.Out);
+		}
+
+		private static void WriteOut(string format, params object[] args)
+		{
+			WriteOut(string.Format(format, args));
+		}
+
+		private static void WriteOutLine(string format, params object[] args)
+		{
+			WriteOut(format, args);
+			Console.Out.WriteLine();
+		}
+
+		private static void WriteError(string message)
+		{
+			Write(message, Console.Error);
+		}
+
+		private static void WriteError(string format, params object[] args)
+		{
+			WriteError(string.Format(format, args));
+		}
+
+		private static void WriteErrorLine(string format, params object[] args)
+		{
+			WriteError(format, args);
+			Console.Error.WriteLine();
+		}
+
+		private const string appName = "KerbalStuffWrapper";
+
+		// TODO: Action methods.
+
+		[Verb(Aliases = "mod,m")]
+		public static void ModInfo([Required]long modId)
+		{
+			Mod mod = KerbalStuff.ModInfo(modId);
+
+			if (mod == null)
+			{
+				WriteErrorLine("Couldn't get Mod info for mod {0}: {1}.", modId, KerbalStuff.currentResponse.StatusDescription);
+			}
+			else
+			{
+				Console.WriteLine(mod);
+			}
+		}
+
+		[Verb(Aliases = "latest,l")]
+		public static void ModLatest([Required]long modId)
+		{
+			ModVersion ver = KerbalStuff.ModLatest(modId);
+
+			if (ver == null)
+			{
+				WriteErrorLine("Couldn't get version info for mod {0}: {1}.", modId, KerbalStuff.currentResponse.StatusDescription);
+			}
+			else
+			{
+				Console.WriteLine(ver);
+			}
+		}
+
+		[Verb(Aliases = "search,s")]
+		public static void ModSearch([Required]string query)
+		{
+			List<Mod> mods = KerbalStuff.ModSearch(query);
+
+			if (mods.Count < 1)
+			{
+				WriteOutLine("Query yielded no results.");
+			}
+			else
+			{
+				foreach (Mod mod in mods)
+				{
+					Console.WriteLine(mod);
+					Console.WriteLine();
+				}
+			}
+		}
+
+		[Verb(Aliases = "user,u")]
+		public static void UserInfo([Required]string username)
+		{
+			User user = KerbalStuff.UserInfo(username);
+
+			if (user == null)
+			{
+				WriteErrorLine("Couldn't get user info for username '{0}': {1}", username, KerbalStuff.currentResponse.StatusDescription);
+			}
+			else
+			{
+				Console.WriteLine(user);
+			}
+		}
+
+		[Verb(Aliases = "us")]
+		public static void UserSearch([Required]string query)
+		{
+			List<User> users = KerbalStuff.UserSearch(query);
+
+			if (users.Count < 1)
+			{
+				WriteOutLine("Query yielded no results.");
+			}
+			else
+			{
+				foreach (User user in users)
+				{
+					Console.WriteLine(user);
+					Console.WriteLine();
+				}
+			}
+		}
+
+		[Verb(Aliases = "create,c")]
+		public static void CreateMod(
+			[Required]
+			[Aliases("user,u")]
+			string username,
+			[Required]
+			[Aliases("pass,p")]
+			string password,
+			[Required]
+			[Aliases("n")]
+			string name,
+			[Required]
+			[Aliases("desc,d")]
+			string short_description,
+			[Required]
+			[Aliases("ver,v")]
+			string version,
+			[Required]
+			[Aliases("ksp,k")]
+			string ksp_version,
+			[Required]
+			[Aliases("lic,l")]
+			string license,
+			[Required]
+			[Aliases("file,f")]
+			string filePath
+		)
+		{
+			Mod mod = new Mod(name, short_description, version, ksp_version, license);
+
+			var loginDict = KerbalStuff.Login(username, password);
+
+			if (loginDict == null)
+			{
+				WriteErrorLine("Could not complete login attempt: {0}", KerbalStuff.currentResponse.StatusDescription);
+			}
+			else if (loginDict.ContainsKey("error") && loginDict["error"].ToString().ToLower() == "true")
+			{
+				WriteErrorLine("Login failed: {0}", loginDict["reason"]);
+			}
+			else
+			{
+				var createDict = KerbalStuff.Create(mod, Path.GetFileName(filePath), filePath);
+
+				if (createDict == null)
+				{
+					WriteErrorLine("Could not complete creation attempt: {0}", KerbalStuff.currentResponse.StatusDescription);
+				}
+				else if (createDict.ContainsKey("error") && createDict["error"].ToString().ToLower() == "true")
+				{
+					WriteErrorLine("Creation failed: {0}", createDict["message"]);
+				}
+				else
+				{
+					WriteOutLine("New mod '{0}' created with id #{2}!  You can view and publish the mod at {1}",
+						createDict["name"],
+						string.Concat(KerbalStuff.RootUri, createDict["url"]),
+						createDict["id"]
+					);
+				}
+			}
+		}
+
+		[Verb(Aliases = "update,up")]
+		public static void UpdateMod(
+			[Required]
+			[Aliases("user,u")]
+			string username,
+			[Required]
+			[Aliases("pass,p")]
+			string password,
+			[Required]
+			[Aliases("mod,m")]
+			long modId,
+			[Required]
+			[Aliases("ver,v")]
+			string version,
+			[Required]
+			[Aliases("ksp,k")]
+			string ksp_version,
+			[Aliases("log,l")]
+			string changelog,
+			[DefaultValue(false)]
+			[Aliases("notify,n")]
+			bool notifyFollowers,
+			[Required]
+			[Aliases("file,f")]
+			string filePath
+		)
+		{
+			ModVersion ver;
+			if (changelog != string.Empty)
+			{
+				ver = new ModVersion(version, ksp_version, changelog);
+			}
+			else
+			{
+				ver = new ModVersion(version, ksp_version);
+			}
+
+			var loginDict = KerbalStuff.Login(username, password);
+
+			if (loginDict == null)
+			{
+				WriteErrorLine("Could not complete login attempt: {0}", KerbalStuff.currentResponse.StatusDescription);
+			}
+			else if (loginDict.ContainsKey("error") && loginDict["error"].ToString().ToLower() == "true")
+			{
+				WriteErrorLine("Login failed: {0}", loginDict["reason"]);
+			}
+			else
+			{
+				var updateDict = KerbalStuff.Update(modId, ver, notifyFollowers, Path.GetFileName(filePath), filePath);
+
+				if (updateDict == null)
+				{
+					WriteErrorLine("Could not complete creation attempt: {0}", KerbalStuff.currentResponse.StatusDescription);
+				}
+				else if (updateDict.ContainsKey("error") && updateDict["error"].ToString().ToLower() == "true")
+				{
+					WriteErrorLine("Creation failed: {0}", updateDict["message"]);
+				}
+				else
+				{
+					WriteOutLine("Mod #{0}!  You can view the update at {1}",
+						updateDict["id"],
+						string.Concat(KerbalStuff.RootUri, updateDict["url"])
+					);
+				}
+			}
+		}
+			
+		[Empty, Help, Global]
+		public static void Help(string help)
+		{
+			Console.Error.WriteLine(string.Format("Usage: {0} <ACTION> /OPTION[:ARG] [/OPTION[:ARG]...]", appName));
+			Console.Error.WriteLine("\nActions:");
+			Console.Error.WriteLine(parser.GetHelpString());
+		}
+
+		[Error]
+		public static void HandleError(ExceptionContext context)
+		{
+			if (context.Exception is VerbNotFoundException)
+			{
+				var ex = context.Exception as VerbNotFoundException;
+				WriteErrorLine("Action '{0}' does not exist.", ex.Verb);
+			}
+			else if (context.Exception is MissingDefaultVerbException)
+			{
+				WriteErrorLine("An action is required.");
+			}
+			else if (context.Exception is UnhandledParametersException)
+			{
+				var ex = context.Exception as UnhandledParametersException;
+				var keys = ex.UnhandledParameters.Keys;
+
+				WriteError("invalid option");
+
+				if (keys.Count > 1)
+				{
+					Console.Error.Write("s");
+				}
+
+				Console.Error.Write(string.Format(" for action {0}: '{1}'", args[0], string.Join(", ", keys.ToArray())));
+			}
+			else if (context.Exception is MissingArgumentPrefixException)
+			{
+				var ex = context.Exception as MissingArgumentPrefixException;
+
+				WriteErrorLine("{0}: option {1}", args[0], ex.Message);
+			}
+			else if (context.Exception is MissingArgumentValueException)
+			{
+				var ex = context.Exception as MissingArgumentValueException;
+
+				WriteErrorLine("{0}: option '{1}' requires an argument.", args[0], ex.ParameterName);
+			}
+			else if (context.Exception is TypeConvertionException)
+			{
+				var ex = context.Exception as TypeConvertionException;
+				WriteErrorLine("Invalid argument for {2}: '{0}' cannot be converted to {1}.\n", ex.Value, ex.Type.HumanName(), args[0]);
+				Help(string.Empty);
+			}
+			else if (context.Exception is CommandLineParserException)
+			{
+				WriteErrorLine(context.Exception.Message);
+			}
+			else
+			{
+				WriteErrorLine("An unexpected error occured.  {0}: {1}", context.Exception.GetType().Name, context.Exception.Message);
+				Console.Error.WriteLine();
+				WriteErrorLine(context.Exception.StackTrace);
+
+				Console.Error.WriteLine();
+				Console.Error.WriteLine("Response:");
+				Console.Error.WriteLine();
+
+				if (KerbalStuff.currentResponse != null)
+				{
+					Console.Error.WriteLine((new StreamReader(KerbalStuff.currentResponse.GetResponseStream())).ReadToEnd());
+				}
+
+				if (KerbalStuff.currentJson != null)
+				{
+					if (KerbalStuff.currentJson is Dictionary<string, object>)
+					{
+						var json = KerbalStuff.currentJson as Dictionary<string, object>;
+						foreach (KeyValuePair<string, object> item in json)
+						{
+							Console.Error.WriteLine(string.Format("{0}: {1}", item.Key, item.Value));
+						}
+					}
+				}
+
+				return;
+			}
+
+			Console.Error.WriteLine();
+
+			Help(string.Empty);
 		}
 	}
 }

--- a/KerbalStuffWrapper/KerbalStuffWrapper.csproj
+++ b/KerbalStuffWrapper/KerbalStuffWrapper.csproj
@@ -7,7 +7,7 @@
     <SchemaVersion>2.0</SchemaVersion>
     <ProjectGuid>{1E93CDA7-56A8-410F-A5A2-0ABB9210CA58}</ProjectGuid>
     <OutputType>Exe</OutputType>
-    <RootNamespace>KerbalStuff</RootNamespace>
+    <RootNamespace>KerbalStuff.Wrapper</RootNamespace>
     <AssemblyName>KerbalStuffWrapper</AssemblyName>
     <UseMSBuildEngine>False</UseMSBuildEngine>
     <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
@@ -35,14 +35,22 @@
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <ItemGroup>
     <Reference Include="System" />
-    <Reference Include="System.Net" />
+    <Reference Include="CLAP">
+      <HintPath>..\..\CLAP.4.6\lib\net35\CLAP.dll</HintPath>
+    </Reference>
   </ItemGroup>
   <ItemGroup>
-    <Compile Include="Program.cs" />
-    <Compile Include="MiniJSON.cs" />
-    <Compile Include="Mod.cs" />
-    <Compile Include="KerbalStuff.cs" />
-    <Compile Include="User.cs" />
-    <Compile Include="FormUpload.cs" />
+    <Compile Include="KerbalStuffWrapper.cs" />
+    <Compile Include="Utils.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\KerbalStuff\KerbalStuff.csproj">
+      <Project>{0CA12F64-AD3B-41E6-8A35-8781A8DAD1C1}</Project>
+      <Name>KerbalStuff</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\KerbalStuffReadOnly\KerbalStuffReadOnly.csproj">
+      <Project>{720FA70F-D785-48ED-BA45-561921E0EEEC}</Project>
+      <Name>KerbalStuffReadOnly</Name>
+    </ProjectReference>
   </ItemGroup>
 </Project>

--- a/KerbalStuffWrapper/MiniJSON.cs
+++ /dev/null
@@ -1,548 +1,1 @@
-/*
- * Copyright (c) 2013 Calvin Rien
- *
- * Based on the JSON parser by Patrick van Bergen
- * http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html
- *
- * Simplified it so that it doesn't throw exceptions
- * and can be used in Unity iPhone with maximum code stripping.
- *
- * Permission is hereby granted, free of charge, to any person obtaining
- * a copy of this software and associated documentation files (the
- * "Software"), to deal in the Software without restriction, including
- * without limitation the rights to use, copy, modify, merge, publish,
- * distribute, sublicense, and/or sell copies of the Software, and to
- * permit persons to whom the Software is furnished to do so, subject to
- * the following conditions:
- *
- * The above copyright notice and this permission notice shall be
- * included in all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
- * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
- * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
- * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
- * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
 
-namespace MiniJSON {
-	// Example usage:
-	//
-	//  using UnityEngine;
-	//  using System.Collections;
-	//  using System.Collections.Generic;
-	//  using MiniJSON;
-	//
-	//  public class MiniJSONTest : MonoBehaviour {
-	//      void Start () {
-	//          var jsonString = "{ \"array\": [1.44,2,3], " +
-	//                          "\"object\": {\"key1\":\"value1\", \"key2\":256}, " +
-	//                          "\"string\": \"The quick brown fox \\\"jumps\\\" over the lazy dog \", " +
-	//                          "\"unicode\": \"\\u3041 Men\u00fa sesi\u00f3n\", " +
-	//                          "\"int\": 65536, " +
-	//                          "\"float\": 3.1415926, " +
-	//                          "\"bool\": true, " +
-	//                          "\"null\": null }";
-	//
-	//          var dict = Json.Deserialize(jsonString) as Dictionary<string,object>;
-	//
-	//          Debug.Log("deserialized: " + dict.GetType());
-	//          Debug.Log("dict['array'][0]: " + ((List<object>) dict["array"])[0]);
-	//          Debug.Log("dict['string']: " + (string) dict["string"]);
-	//          Debug.Log("dict['float']: " + (double) dict["float"]); // floats come out as doubles
-	//          Debug.Log("dict['int']: " + (long) dict["int"]); // ints come out as longs
-	//          Debug.Log("dict['unicode']: " + (string) dict["unicode"]);
-	//
-	//          var str = Json.Serialize(dict);
-	//
-	//          Debug.Log("serialized: " + str);
-	//      }
-	//  }
-
-	/// <summary>
-	/// This class encodes and decodes JSON strings.
-	/// Spec. details, see http://www.json.org/
-	///
-	/// JSON uses Arrays and Objects. These correspond here to the datatypes IList and IDictionary.
-	/// All numbers are parsed to doubles.
-	/// </summary>
-	public static class Json {
-		/// <summary>
-		/// Parses the string json into a value
-		/// </summary>
-		/// <param name="json">A JSON string.</param>
-		/// <returns>An List&lt;object&gt;, a Dictionary&lt;string, object&gt;, a double, an integer,a string, null, true, or false</returns>
-		public static object Deserialize(string json) {
-			// save the string for debug information
-			if (json == null) {
-				return null;
-			}
-
-			return Parser.Parse(json);
-		}
-
-		sealed class Parser : IDisposable {
-			const string WORD_BREAK = "{}[],:\"";
-
-			public static bool IsWordBreak(char c) {
-				return Char.IsWhiteSpace(c) || WORD_BREAK.IndexOf(c) != -1;
-			}
-
-			enum TOKEN {
-				NONE,
-				CURLY_OPEN,
-				CURLY_CLOSE,
-				SQUARED_OPEN,
-				SQUARED_CLOSE,
-				COLON,
-				COMMA,
-				STRING,
-				NUMBER,
-				TRUE,
-				FALSE,
-				NULL
-			};
-
-			StringReader json;
-
-			Parser(string jsonString) {
-				json = new StringReader(jsonString);
-			}
-
-			public static object Parse(string jsonString) {
-				using (var instance = new Parser(jsonString)) {
-					return instance.ParseValue();
-				}
-			}
-
-			public void Dispose() {
-				json.Dispose();
-				json = null;
-			}
-
-			Dictionary<string, object> ParseObject() {
-				Dictionary<string, object> table = new Dictionary<string, object>();
-
-				// ditch opening brace
-				json.Read();
-
-				// {
-				while (true) {
-					switch (NextToken) {
-						case TOKEN.NONE:
-							return null;
-						case TOKEN.COMMA:
-							continue;
-						case TOKEN.CURLY_CLOSE:
-							return table;
-						default:
-							// name
-							string name = ParseString();
-							if (name == null) {
-								return null;
-							}
-
-							// :
-							if (NextToken != TOKEN.COLON) {
-								return null;
-							}
-							// ditch the colon
-							json.Read();
-
-							// value
-							table[name] = ParseValue();
-							break;
-					}
-				}
-			}
-
-			List<object> ParseArray() {
-				List<object> array = new List<object>();
-
-				// ditch opening bracket
-				json.Read();
-
-				// [
-				var parsing = true;
-				while (parsing) {
-					TOKEN nextToken = NextToken;
-
-					switch (nextToken) {
-						case TOKEN.NONE:
-							return null;
-						case TOKEN.COMMA:
-							continue;
-						case TOKEN.SQUARED_CLOSE:
-							parsing = false;
-							break;
-						default:
-							object value = ParseByToken(nextToken);
-
-							array.Add(value);
-							break;
-					}
-				}
-
-				return array;
-			}
-
-			object ParseValue() {
-				TOKEN nextToken = NextToken;
-				return ParseByToken(nextToken);
-			}
-
-			object ParseByToken(TOKEN token) {
-				switch (token) {
-					case TOKEN.STRING:
-						return ParseString();
-					case TOKEN.NUMBER:
-						return ParseNumber();
-					case TOKEN.CURLY_OPEN:
-						return ParseObject();
-					case TOKEN.SQUARED_OPEN:
-						return ParseArray();
-					case TOKEN.TRUE:
-						return true;
-					case TOKEN.FALSE:
-						return false;
-					case TOKEN.NULL:
-						return null;
-					default:
-						return null;
-				}
-			}
-
-			string ParseString() {
-				StringBuilder s = new StringBuilder();
-				char c;
-
-				// ditch opening quote
-				json.Read();
-
-				bool parsing = true;
-				while (parsing) {
-
-					if (json.Peek() == -1) {
-						parsing = false;
-						break;
-					}
-
-					c = NextChar;
-					switch (c) {
-						case '"':
-							parsing = false;
-							break;
-						case '\\':
-							if (json.Peek() == -1) {
-								parsing = false;
-								break;
-							}
-
-							c = NextChar;
-							switch (c) {
-								case '"':
-								case '\\':
-								case '/':
-									s.Append(c);
-									break;
-								case 'b':
-									s.Append('\b');
-									break;
-								case 'f':
-									s.Append('\f');
-									break;
-								case 'n':
-									s.Append('\n');
-									break;
-								case 'r':
-									s.Append('\r');
-									break;
-								case 't':
-									s.Append('\t');
-									break;
-								case 'u':
-									var hex = new char[4];
-
-									for (int i=0; i< 4; i++) {
-										hex[i] = NextChar;
-									}
-
-									s.Append((char) Convert.ToInt32(new string(hex), 16));
-									break;
-							}
-							break;
-						default:
-							s.Append(c);
-							break;
-					}
-				}
-
-				return s.ToString();
-			}
-
-			object ParseNumber() {
-				string number = NextWord;
-
-				if (number.IndexOf('.') == -1) {
-					long parsedInt;
-					Int64.TryParse(number, out parsedInt);
-					return parsedInt;
-				}
-
-				double parsedDouble;
-				Double.TryParse(number, out parsedDouble);
-				return parsedDouble;
-			}
-
-			void EatWhitespace() {
-				while (Char.IsWhiteSpace(PeekChar)) {
-					json.Read();
-
-					if (json.Peek() == -1) {
-						break;
-					}
-				}
-			}
-
-			char PeekChar {
-				get {
-					return Convert.ToChar(json.Peek());
-				}
-			}
-
-			char NextChar {
-				get {
-					return Convert.ToChar(json.Read());
-				}
-			}
-
-			string NextWord {
-				get {
-					StringBuilder word = new StringBuilder();
-
-					while (!IsWordBreak(PeekChar)) {
-						word.Append(NextChar);
-
-						if (json.Peek() == -1) {
-							break;
-						}
-					}
-
-					return word.ToString();
-				}
-			}
-
-			TOKEN NextToken {
-				get {
-					EatWhitespace();
-
-					if (json.Peek() == -1) {
-						return TOKEN.NONE;
-					}
-
-					switch (PeekChar) {
-						case '{':
-							return TOKEN.CURLY_OPEN;
-						case '}':
-							json.Read();
-							return TOKEN.CURLY_CLOSE;
-						case '[':
-							return TOKEN.SQUARED_OPEN;
-						case ']':
-							json.Read();
-							return TOKEN.SQUARED_CLOSE;
-						case ',':
-							json.Read();
-							return TOKEN.COMMA;
-						case '"':
-							return TOKEN.STRING;
-						case ':':
-							return TOKEN.COLON;
-						case '0':
-						case '1':
-						case '2':
-						case '3':
-						case '4':
-						case '5':
-						case '6':
-						case '7':
-						case '8':
-						case '9':
-						case '-':
-							return TOKEN.NUMBER;
-					}
-
-					switch (NextWord) {
-						case "false":
-							return TOKEN.FALSE;
-						case "true":
-							return TOKEN.TRUE;
-						case "null":
-							return TOKEN.NULL;
-					}
-
-					return TOKEN.NONE;
-				}
-			}
-		}
-
-		/// <summary>
-		/// Converts a IDictionary / IList object or a simple type (string, int, etc.) into a JSON string
-		/// </summary>
-		/// <param name="json">A Dictionary&lt;string, object&gt; / List&lt;object&gt;</param>
-		/// <returns>A JSON encoded string, or null if object 'json' is not serializable</returns>
-		public static string Serialize(object obj) {
-			return Serializer.Serialize(obj);
-		}
-
-		sealed class Serializer {
-			StringBuilder builder;
-
-			Serializer() {
-				builder = new StringBuilder();
-			}
-
-			public static string Serialize(object obj) {
-				var instance = new Serializer();
-
-				instance.SerializeValue(obj);
-
-				return instance.builder.ToString();
-			}
-
-			void SerializeValue(object value) {
-				IList asList;
-				IDictionary asDict;
-				string asStr;
-
-				if (value == null) {
-					builder.Append("null");
-				} else if ((asStr = value as string) != null) {
-					SerializeString(asStr);
-				} else if (value is bool) {
-					builder.Append((bool) value ? "true" : "false");
-				} else if ((asList = value as IList) != null) {
-					SerializeArray(asList);
-				} else if ((asDict = value as IDictionary) != null) {
-					SerializeObject(asDict);
-				} else if (value is char) {
-					SerializeString(new string((char) value, 1));
-				} else {
-					SerializeOther(value);
-				}
-			}
-
-			void SerializeObject(IDictionary obj) {
-				bool first = true;
-
-				builder.Append('{');
-
-				foreach (object e in obj.Keys) {
-					if (!first) {
-						builder.Append(',');
-					}
-
-					SerializeString(e.ToString());
-					builder.Append(':');
-
-					SerializeValue(obj[e]);
-
-					first = false;
-				}
-
-				builder.Append('}');
-			}
-
-			void SerializeArray(IList anArray) {
-				builder.Append('[');
-
-				bool first = true;
-
-				foreach (object obj in anArray) {
-					if (!first) {
-						builder.Append(',');
-					}
-
-					SerializeValue(obj);
-
-					first = false;
-				}
-
-				builder.Append(']');
-			}
-
-			void SerializeString(string str) {
-				builder.Append('\"');
-
-				char[] charArray = str.ToCharArray();
-				foreach (var c in charArray) {
-					switch (c) {
-						case '"':
-							builder.Append("\\\"");
-							break;
-						case '\\':
-							builder.Append("\\\\");
-							break;
-						case '\b':
-							builder.Append("\\b");
-							break;
-						case '\f':
-							builder.Append("\\f");
-							break;
-						case '\n':
-							builder.Append("\\n");
-							break;
-						case '\r':
-							builder.Append("\\r");
-							break;
-						case '\t':
-							builder.Append("\\t");
-							break;
-						default:
-							int codepoint = Convert.ToInt32(c);
-							if ((codepoint >= 32) && (codepoint <= 126)) {
-								builder.Append(c);
-							} else {
-								builder.Append("\\u");
-								builder.Append(codepoint.ToString("x4"));
-							}
-							break;
-					}
-				}
-
-				builder.Append('\"');
-			}
-
-			void SerializeOther(object value) {
-				// NOTE: decimals lose precision during serialization.
-				// They always have, I'm just letting you know.
-				// Previously floats and doubles lost precision too.
-				if (value is float) {
-					builder.Append(((float) value).ToString("R"));
-				} else if (value is int
-					|| value is uint
-					|| value is long
-					|| value is sbyte
-					|| value is byte
-					|| value is short
-					|| value is ushort
-					|| value is ulong) {
-					builder.Append(value);
-				} else if (value is double
-					|| value is decimal) {
-					builder.Append(Convert.ToDouble(value).ToString("R"));
-				} else {
-					SerializeString(value.ToString());
-				}
-			}
-		}
-	}
-}
-

--- a/KerbalStuffWrapper/Mod.cs
+++ /dev/null
@@ -1,166 +1,1 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 
-namespace KerbalStuff
-{
-	public class Mod
-	{
-		public long downloads
-		{
-			get;
-			private set;
-		}
-
-		public string name
-		{
-			get;
-			private set;
-		}
-
-		public long followers
-		{
-			get;
-			private set;
-		}
-
-		public string author
-		{
-			get;
-			private set;
-		}
-
-		public long default_version_id
-		{
-			get;
-			private set;
-		}
-
-		public List<ModVersion> versions
-		{
-			get;
-			private set;
-		}
-
-		public long id
-		{
-			get;
-			private set;
-		}
-
-		public string short_description
-		{
-			get;
-			private set;
-		}
-
-		public string license
-		{
-			get;
-			private set;
-		}
-
-		public Mod(Dictionary<string, object> jsonDict) : this()
-		{
-			this.downloads = (long)jsonDict["downloads"];
-			this.name = (string)jsonDict["name"];
-			this.followers = (long)jsonDict["followers"];
-			this.author = (string)jsonDict["author"];
-			this.default_version_id = (long)jsonDict["default_version_id"];
-			this.id = (long)jsonDict["id"];
-			this.short_description = (string)jsonDict["short_description"];
-
-			if (jsonDict.ContainsKey("versions"))
-			{
-				foreach (var ver in (jsonDict["versions"] as List<object>))
-				{
-					if (ver is Dictionary<string, object>)
-					{
-						this.versions.Add(new ModVersion(ver as Dictionary<string, object>));
-					}
-				}
-			}
-		}
-
-		public Mod(string name, string short_description, string version, string ksp_version, string license) : this()
-		{
-			this.name = name;
-			this.short_description = short_description;
-			this.license = license;
-
-			this.versions.Add(new ModVersion(version, ksp_version));
-		}
-
-		private Mod()
-		{
-			this.versions = new List<ModVersion>();
-		}
-
-		public override string ToString()
-		{
-			return string.Format("Mod: {1}\nid: {6}\nauthor: {3}\ndownloads: {0}\nfollowers: {2}\nshort_description: {7}\ndefault_version_id: {4}\nversions:\n[\n{5}\n]\n", downloads, name, followers, author, default_version_id, string.Join("\n", versions.Select(v => v.ToString()).ToArray()), id, short_description);
-		}
-	}
-
-	public class ModVersion
-	{
-		public string changelog
-		{
-			get;
-			private set;
-		}
-
-		public string ksp_version
-		{
-			get;
-			private set;
-		}
-
-		public string download_path
-		{
-			get;
-			private set;
-		}
-
-		public long id
-		{
-			get;
-			private set;
-		}
-
-		public string friendly_version
-		{
-			get;
-			private set;
-		}
-
-		public ModVersion(Dictionary<string, object> jsonDict) : this()
-		{
-			this.changelog = (string)jsonDict["changelog"];
-			this.ksp_version = (string)jsonDict["ksp_version"];
-			this.download_path = (string)jsonDict["download_path"];
-			this.id = (long)jsonDict["id"];
-			this.friendly_version = (string)jsonDict["friendly_version"];
-		}
-
-		public ModVersion(string version, string ksp_version, string changelog) : this(version, ksp_version)
-		{
-			this.changelog = changelog;
-		}
-
-		public ModVersion(string version, string ksp_version) : this()
-		{
-			this.friendly_version = version;
-			this.ksp_version = ksp_version;
-		}
-
-		private ModVersion() {}
-
-		public override string ToString()
-		{
-			return string.Format("ModVersion {4}:\nid: {3}\nksp_version: {1}\ndownload_path: {2}\nchangelog: {0}", changelog, ksp_version, download_path, id, friendly_version);
-		}
-	}
-}
-
-

--- a/KerbalStuffWrapper/Program.cs
+++ /dev/null
@@ -1,60 +1,1 @@
-using System;
-using System.Collections.Generic;
-using KerbalStuff;
 
-// TODO: Get rid of this whole file and compile as a library instead of an executable.
-
-namespace KSWUnitTest
-{
-	using KerbalStuff;
-
-	public static class Program
-	{
-		public static int Main()
-		{
-			/*Console.WriteLine("\n\nSearch:");
-			foreach (Mod obj in KerbalStuff.Search("Quantum Struts"))
-			{
-				Console.WriteLine(obj);
-			}
-
-			Console.WriteLine("\n\nModInfo:");
-			Console.WriteLine(KerbalStuff.ModInfo(123));
-
-			Console.WriteLine("\n\nModLatest:");
-			Console.WriteLine(KerbalStuff.ModLatest(24));
-
-			Console.WriteLine("\n\nUserInfo:");
-			Console.WriteLine(KerbalStuff.UserInfo("toadicus"));
-
-			Console.WriteLine("\n\nUserSearch:");
-			foreach (User user in KerbalStuff.UserSearch("toad"))
-			{
-				Console.WriteLine(user);
-			}
-
-			KerbalStuff.Login("toadicus", "redacted");
-
-			var testMod = new Mod(
-				"testMod",
-				"This mod is a test.",
-				"0.1",
-				"0.24.2",
-				"MIT"
-			);
-
-			KerbalStuff.Create(
-				testMod,
-				"mod.zip",
-				"mod.zip"
-			);
-
-			KerbalStuff.Update(210, new ModVersion("0.3", "0.24.2", "changes changes changes!"), false, "mod.zip", "mod.zip");
-			*/
-
-			return 0;
-		}
-	}
-}
-
-

--- a/KerbalStuffWrapper/User.cs
+++ /dev/null
@@ -1,92 +1,1 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
 
-namespace KerbalStuff
-{
-	public class User
-	{
-		public string username
-		{
-			get;
-			private set;
-		}
-
-		public string twitterUsername
-		{
-			get;
-			private set;
-		}
-
-		public List<Mod> mods
-		{
-			get;
-			private set;
-		}
-
-		public string redditUsername
-		{
-			get;
-			private set;
-		}
-
-		public string ircNick
-		{
-			get;
-			private set;
-		}
-
-		public string description
-		{
-			get;
-			private set;
-		}
-
-		public string forumUsername
-		{
-			get;
-			private set;
-		}
-
-		public User(Dictionary<string, object> jsonDict) : this()
-		{
-			this.username = (string)jsonDict["username"];
-			this.twitterUsername = (string)jsonDict["twitterUsername"];
-			this.redditUsername = (string)jsonDict["redditUsername"];
-			this.ircNick = (string)jsonDict["ircNick"];
-			this.forumUsername = (string)jsonDict["forumUsername"];
-
-			this.description = (string)jsonDict["description"];
-
-			this.mods = new List<Mod>();
-
-			foreach (object modObj in (jsonDict["mods"] as List<object>))
-			{
-				this.mods.Add(new Mod(modObj as Dictionary<string, object>));
-			}
-		}
-
-		public User(object jsonObj) : this((Dictionary<string, object>)jsonObj) {}
-
-		private User() {}
-
-		public override string ToString()
-		{
-			return string.Format(
-				"User: username={0}, twitterUsername={1}, redditUsername={3}, ircNick={4}, description={5}, forumUsername={6}\nmods:\n{2}",
-				username,
-				twitterUsername,
-				string.Join(
-					"\n",
-					mods.Select(m => m.ToString()).ToArray()
-				),
-				redditUsername,
-				ircNick,
-				description,
-				forumUsername
-			);
-		}
-	}
-}
-
-

--- /dev/null
+++ b/KerbalStuffWrapper/Utils.cs
@@ -1,1 +1,30 @@
+using CLAP;
+using CLAP.Interception;
+using CLAP.Validation;
+using System;
+using System.IO;
+using System.Collections.Generic;
 
+namespace KerbalStuff.Wrapper
+{
+	public static class Utils
+	{
+		public static string HumanName(this Type type)
+		{
+			switch (type.Name)
+			{
+				case "Int64":
+				case "Int32":
+				case "Int16":
+					return "integer";
+				case "Single":
+				case "Double":
+					return "decimal number";
+				default:
+					return type.Name.ToLower();
+			}
+		}
+	}
+}
+
+