diff --git a/ui/install-link-generator/index.html b/ui/install-link-generator/index.html
new file mode 100644
index 00000000..9a73ce0e
--- /dev/null
+++ b/ui/install-link-generator/index.html
@@ -0,0 +1,72 @@
+
+
+
+
+
+ Goose Install Link Generator
+
+
+
+
+
Install Link Generator
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/install-link-generator/script.js b/ui/install-link-generator/script.js
new file mode 100644
index 00000000..6f4147b0
--- /dev/null
+++ b/ui/install-link-generator/script.js
@@ -0,0 +1,152 @@
+document.addEventListener('DOMContentLoaded', () => {
+ // Tab switching
+ const tabs = document.querySelectorAll('.tab-btn');
+ tabs.forEach(tab => {
+ tab.addEventListener('click', () => {
+ tabs.forEach(t => t.classList.remove('active'));
+ tab.classList.add('active');
+
+ document.querySelectorAll('.tab-content').forEach(content => {
+ content.classList.remove('active');
+ });
+ document.getElementById(tab.dataset.tab).classList.add('active');
+ });
+ });
+
+ // Handle built-in checkbox
+ const isBuiltinCheckbox = document.getElementById('isBuiltin');
+ const nonBuiltinFields = document.querySelector('.non-builtin');
+
+ isBuiltinCheckbox.addEventListener('change', () => {
+ nonBuiltinFields.style.display = isBuiltinCheckbox.checked ? 'none' : 'block';
+ });
+
+ // Environment variables handling
+ const envVarsContainer = document.getElementById('envVars');
+ const addEnvVarBtn = document.getElementById('addEnvVar');
+
+ function createEnvVarInputs() {
+ const envVarDiv = document.createElement('div');
+ envVarDiv.className = 'env-var';
+
+ const nameInput = document.createElement('input');
+ nameInput.type = 'text';
+ nameInput.placeholder = 'Variable Name';
+ nameInput.className = 'env-name';
+
+ const descInput = document.createElement('input');
+ descInput.type = 'text';
+ descInput.placeholder = 'Description';
+ descInput.className = 'env-desc';
+
+ const removeBtn = document.createElement('button');
+ removeBtn.type = 'button';
+ removeBtn.textContent = 'Remove';
+ removeBtn.onclick = () => envVarDiv.remove();
+
+ envVarDiv.appendChild(nameInput);
+ envVarDiv.appendChild(descInput);
+ envVarDiv.appendChild(removeBtn);
+
+ return envVarDiv;
+ }
+
+ addEnvVarBtn.addEventListener('click', () => {
+ envVarsContainer.appendChild(createEnvVarInputs());
+ });
+
+ // Generate link from form
+ document.getElementById('installForm').addEventListener('submit', (e) => {
+ e.preventDefault();
+ const formData = new FormData(e.target);
+ const server = {
+ is_builtin: formData.get('is_builtin') === 'on',
+ id: formData.get('id'),
+ name: formData.get('name'),
+ description: formData.get('description'),
+ command: formData.get('command'),
+ environmentVariables: []
+ };
+
+ // Collect environment variables
+ document.querySelectorAll('.env-var').forEach(envVar => {
+ const name = envVar.querySelector('.env-name').value;
+ const description = envVar.querySelector('.env-desc').value;
+ if (name && description) {
+ server.environmentVariables.push({
+ name,
+ description,
+ required: true
+ });
+ }
+ });
+
+ const link = generateInstallLink(server);
+ displayGeneratedLink(link);
+ });
+
+ // Generate link from JSON
+ document.getElementById('generateFromJson').addEventListener('click', () => {
+ try {
+ const jsonInput = document.getElementById('jsonInput').value;
+ const server = JSON.parse(jsonInput);
+ const link = generateInstallLink(server);
+ displayGeneratedLink(link);
+ } catch (error) {
+ alert('Invalid JSON: ' + error.message);
+ }
+ });
+
+ // Link generation logic
+ function generateInstallLink(server) {
+ if (server.is_builtin) {
+ const queryParams = [
+ 'cmd=goosed',
+ 'arg=mcp',
+ `arg=${encodeURIComponent(server.id)}`,
+ `description=${encodeURIComponent(server.id)}`
+ ].join('&');
+ return `goose://extension?${queryParams}`;
+ }
+
+ const parts = server.command.split(" ");
+ const baseCmd = parts[0];
+ const args = parts.slice(1);
+ const queryParams = [
+ `cmd=${encodeURIComponent(baseCmd)}`,
+ ...args.map((arg) => `arg=${encodeURIComponent(arg)}`),
+ `id=${encodeURIComponent(server.id)}`,
+ `name=${encodeURIComponent(server.name)}`,
+ `description=${encodeURIComponent(server.description)}`,
+ ...server.environmentVariables
+ .filter((env) => env.required)
+ .map(
+ (env) => `env=${encodeURIComponent(`${env.name}=${env.description}`)}`
+ ),
+ ].join("&");
+
+ return `goose://extension?${queryParams}`;
+ }
+
+ function displayGeneratedLink(link) {
+ const linkElement = document.getElementById('generatedLink');
+ linkElement.textContent = link;
+ }
+
+ // Add sample JSON to the textarea
+ const sampleJson = {
+ is_builtin: false,
+ id: "example-extension",
+ name: "Example Extension",
+ description: "An example Goose extension",
+ command: "npx @gooseai/example-extension",
+ environmentVariables: [
+ {
+ name: "API_KEY",
+ description: "Your API key",
+ required: true
+ }
+ ]
+ };
+ document.getElementById('jsonInput').value = JSON.stringify(sampleJson, null, 2);
+});
\ No newline at end of file
diff --git a/ui/install-link-generator/styles.css b/ui/install-link-generator/styles.css
new file mode 100644
index 00000000..b9638c49
--- /dev/null
+++ b/ui/install-link-generator/styles.css
@@ -0,0 +1,258 @@
+:root {
+ --primary: #ffffff;
+ --primary-dark: #e0e0e0;
+ --secondary: #333333;
+ --text: #ffffff;
+ --background: #000000;
+ --card-bg: #111111;
+ --success: #ffffff;
+ --error: #ff4b4b;
+ --border-color: rgba(255, 255, 255, 0.1);
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ transition: all 0.2s ease;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+ line-height: 1.5;
+ padding: 16px;
+ background: var(--background);
+ color: var(--text);
+ min-height: 100vh;
+}
+
+.container {
+ max-width: 600px;
+ margin: 0 auto;
+ background: var(--card-bg);
+ padding: 1.5rem;
+ border-radius: 12px;
+ border: 1px solid var(--border-color);
+}
+
+h1 {
+ text-align: center;
+ margin-bottom: 1.5rem;
+ color: var(--text);
+ font-size: 1.75rem;
+ font-weight: 600;
+ letter-spacing: -0.5px;
+}
+
+.tabs {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1.5rem;
+ padding: 0.25rem;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+}
+
+.tab-btn {
+ flex: 1;
+ padding: 0.75rem;
+ border: none;
+ background: transparent;
+ color: var(--text);
+ cursor: pointer;
+ border-radius: 6px;
+ font-weight: 500;
+ font-size: 0.875rem;
+}
+
+.tab-btn.active {
+ background: var(--secondary);
+}
+
+.tab-btn:hover:not(.active) {
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.tab-content {
+ display: none;
+ animation: fadeIn 0.2s ease;
+}
+
+.tab-content.active {
+ display: block;
+}
+
+.form-group {
+ margin-bottom: 1rem;
+}
+
+label {
+ display: block;
+ margin-bottom: 0.25rem;
+ font-weight: 500;
+ color: var(--text);
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+input[type="text"],
+textarea {
+ width: 100%;
+ padding: 0.75rem;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ color: var(--text);
+ font-size: 0.875rem;
+}
+
+input[type="text"]:focus,
+textarea:focus {
+ outline: none;
+ border-color: var(--primary);
+}
+
+input[type="checkbox"] {
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ border: 2px solid var(--border-color);
+ border-radius: 4px;
+ cursor: pointer;
+ position: relative;
+}
+
+input[type="checkbox"]:checked {
+ background-color: var(--primary);
+ border-color: var(--primary);
+}
+
+input[type="checkbox"]:checked::after {
+ content: "✓";
+ position: absolute;
+ color: var(--background);
+ font-size: 12px;
+ left: 2px;
+ top: -2px;
+}
+
+textarea {
+ resize: vertical;
+ min-height: 100px;
+}
+
+button {
+ background: var(--secondary);
+ color: white;
+ padding: 0.75rem 1.5rem;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.875rem;
+ font-weight: 500;
+ letter-spacing: 0.5px;
+}
+
+button:hover {
+ background: #444444;
+}
+
+button:active {
+ transform: translateY(1px);
+}
+
+.result {
+ margin-top: 1.5rem;
+ padding-top: 1.5rem;
+ border-top: 1px solid var(--border-color);
+}
+
+#generatedLink {
+ padding: 1rem;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 6px;
+ word-break: break-all;
+ font-family: 'SF Mono', 'Fira Code', monospace;
+ font-size: 0.8125rem;
+ border: 1px solid var(--border-color);
+}
+
+.env-var {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 0.75rem;
+ align-items: center;
+}
+
+.env-var input {
+ flex: 1;
+}
+
+.env-var button {
+ padding: 0.75rem;
+ min-width: 40px;
+ background: #333333;
+}
+
+#addEnvVar {
+ width: 100%;
+ margin-top: 0.75rem;
+ background: #333333;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(5px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Scrollbar styling */
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #444444;
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #555555;
+}
+
+/* Focus styles */
+*:focus-visible {
+ outline: 2px solid var(--primary);
+ outline-offset: 2px;
+}
+
+/* Responsive adjustments */
+@media (max-width: 640px) {
+ .container {
+ padding: 1rem;
+ }
+
+ h1 {
+ font-size: 1.5rem;
+ }
+
+ .tabs {
+ flex-direction: column;
+ }
+
+ button {
+ width: 100%;
+ }
+}
\ No newline at end of file