From fd7361e04466ed2c7b381ca57fc553951b8cfd64 Mon Sep 17 00:00:00 2001 From: Alfred Date: Tue, 7 Apr 2026 11:40:25 -0400 Subject: [PATCH] Initial commit: Alfred Commander v1.0.1 - VS Code extension - AI chat sidebar with multi-provider support - Session management, token tracking, usage stats - Voice input, file attachments, code actions - Integrates with Alfred IDE (code-server) auth system --- .gitignore | 3 + .vsixmanifest | 41 + README.md | 60 + extension.js | 2093 ++++++++++++++++++++++++++++++ extensions.json | 12 + media/alfred-icon.svg | 6 + media/walkthrough/meet-alfred.md | 17 + media/walkthrough/models.md | 18 + media/walkthrough/shortcuts.md | 24 + media/walkthrough/sovereign.md | 27 + media/walkthrough/stats.md | 16 + media/walkthrough/voice.md | 18 + package.json | 137 ++ 13 files changed, 2472 insertions(+) create mode 100644 .gitignore create mode 100644 .vsixmanifest create mode 100644 README.md create mode 100644 extension.js create mode 100644 extensions.json create mode 100644 media/alfred-icon.svg create mode 100644 media/walkthrough/meet-alfred.md create mode 100644 media/walkthrough/models.md create mode 100644 media/walkthrough/shortcuts.md create mode 100644 media/walkthrough/sovereign.md create mode 100644 media/walkthrough/stats.md create mode 100644 media/walkthrough/voice.md create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d8b38d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.vscode-test/ +*.vsix diff --git a/.vsixmanifest b/.vsixmanifest new file mode 100644 index 0000000..c2e06a7 --- /dev/null +++ b/.vsixmanifest @@ -0,0 +1,41 @@ + + + + + Alfred Commander — Full IDE Chat & Voice + Fresh Alfred panel: all agents & models, voice STT/TTS, attachments, hands-free. IDE shortcuts are explicit buttons — chat text always goes to AI. + keybindings + Other + Public + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1bd50f6 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Alfred Commander (Theia / code-server extension) + +**Alfred IDE** = GoCodeMe **Theia** in the browser (`/ide/...`), **not** Cursor. Extensions are loaded from **`gocodeme/theia-fork/plugins/`** (see `gocodeme/docs/ALFRED_COMMANDER_THEIA.md`). + +**New panel** — does **not** replace `gositeme.alfred-voice`; you can disable the old one when you’re happy. + +## What’s included (full stack) + +- **All agents & model routes** (same list as Alfred Voice) +- **Voice**: mic → Whisper STT → chat; **TTS** playback of replies +- **Hands-free**, **attachments** (images, PDF, text, ZIP), **payload meter**, **telemetry log**, **retry** +- **IDE shortcuts** as **explicit buttons** (Terminal, Save, Save all, Commands, Split, New file) — **chat text is never hijacked** by “save/search/find” phrases +- `run ` still sends **only** to the integrated terminal (not to AI) +- `insert` / `type` prefixes still insert at cursor + +## Proven wiring (kept from working code) + +- `POST /api/alfred-chat.php` with `ide_session_token`, IDE bearer headers, **Set-Cookie array** handling, CSRF retry +- `ide_client_id` / `ide_sig` HMAC when profile is available +- **Request id** on thinking rows for reply alignment + +## Install (Alfred IDE / Theia — primary) + +Canonical copy: + +`~/.local/share/code-server/extensions/gositeme.alfred-commander-1.0.0/` + +**Theia** (Alfred IDE) loads plugins from: + +`gocodeme/theia-fork/plugins/gositeme.alfred-commander-1.0.0` → symlink to the canonical path above. + +1. Confirm that symlink exists on the server. +2. **Restart your IDE session** (stop + launch Theia) so plugins reload. +3. **View → Open View…** → **Alfred Commander**. + +Optional: **code-server**-only installs can use the same canonical path under `~/.local/share/code-server/extensions/`. + +4. **Activity bar** — gold mic **Alfred Commander**; or **Command Palette** → **Alfred Commander: Open Panel**. +5. Optional: disable `alfred-voice` if you don’t want two Alfred panels. +6. To dock on the **right**: drag the view into the **secondary side bar** (View → Appearance → Secondary Side Bar). + +## Commands & keys (no **View** menu required) + +- **Ctrl+Shift+P** (or **F1**) → type **`Alfred Commander: Open Panel`** → Enter. + This works even when the top menu bar is hidden. +- **Ctrl+Shift+Alt+O** — **Open Panel** (after extension loads / VSIX updated). +- **Ctrl+Shift+Alt+A** — **Toggle mic** +- **Alfred Commander: Test API Connection** — from Command Palette + +If the menu bar is missing: **Ctrl+Shift+P** → **Preferences: Open Settings** → search **`menu bar`** → set **Menu Bar Visibility** to **Visible** (or **Classic**). The extension also sets a default to show the menu when possible. + +## Commands (palette) + +- `Alfred Commander: Open Panel` +- `Alfred Commander: Toggle Mic` +- `Alfred Commander: Test API Connection` + +## Token + +Same as before: sign in at `/alfred-ide/`, `session.json` in `logs/alfred-ide/` or `~/.alfred-ide/`, or `ALFRED_IDE_TOKEN` env. diff --git a/extension.js b/extension.js new file mode 100644 index 0000000..d127b1f --- /dev/null +++ b/extension.js @@ -0,0 +1,2093 @@ +const vscode = require('vscode'); +const https = require('https'); +const http = require('http'); +const os = require('os'); +const fs = require('fs'); +const crypto = require('crypto'); + +let currentAgent = 'alfred'; +let convId = null; +let csrfToken = null; +let sessionCookie = null; +let userProfile = null; +let hmacSecretCache = null; + +const IDE_SESSION_BRIDGES = [ + '/home/gositeme/domains/gositeme.com/logs/alfred-ide/session.json', + '/home/gositeme/.alfred-ide/session.json' +]; + +function getAlfredHmacSecret() { + if (hmacSecretCache) return hmacSecretCache; + const candidates = [ + '/home/gositeme/domains/gocodeme.com/public_html/.env', + '/home/gositeme/domains/gositeme.com/public_html/gocodeme/mcp-server/.env', + ]; + for (const p of candidates) { + try { + if (!fs.existsSync(p)) continue; + const txt = fs.readFileSync(p, 'utf8'); + const m = txt.match(/ALFRED_HMAC_SECRET=([^\r\n]+)/); + if (m && m[1]) { + hmacSecretCache = String(m[1]).trim(); + return hmacSecretCache; + } + } catch (_) {} + } + hmacSecretCache = process.env.ALFRED_HMAC_SECRET || 'gositeme-alfred-hmac-2026'; + return hmacSecretCache; +} + +function buildIdeIdentityPayload() { + try { + if (!userProfile || !userProfile.client_id) return null; + const cid = String(userProfile.client_id).replace(/[^0-9]/g, ''); + if (!cid) return null; + const name = String(userProfile.name || userProfile.user || 'User').replace(/[^a-zA-Z0-9 _\-.]/g, '').slice(0, 120); + const ts = Math.floor(Date.now() / 1000); + const secret = getAlfredHmacSecret(); + const sig = crypto.createHmac('sha256', secret).update(`${cid}|${name}|${ts}`).digest('hex'); + return { + ide_client_id: parseInt(cid, 10), + ide_name: name, + ide_ts: ts, + ide_sig: sig, + ide_email: String(userProfile.email || '').slice(0, 190), + }; + } catch (_) { + return null; + } +} + +function readLocalIdeToken() { + const envToken = (process.env.ALFRED_IDE_TOKEN || '').trim(); + if (envToken) return envToken; + try { + for (const bridgePath of IDE_SESSION_BRIDGES) { + if (!fs.existsSync(bridgePath)) continue; + const raw = fs.readFileSync(bridgePath, 'utf8'); + const data = JSON.parse(raw || '{}'); + const token = String(data.token || '').trim(); + const exp = Number(data.expires_at || 0); + if (!token) continue; + if (exp > 0 && Date.now() >= (exp * 1000)) continue; + return token; + } + return ''; + } catch (_) { + return ''; + } +} + + +function activate(context) { + const provider = new AlfredCommanderProvider(context.extensionUri, context); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider('alfred-commander.panel', provider, { + webviewOptions: { retainContextWhenHidden: true } + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand('alfred-commander.open', () => { + vscode.commands.executeCommand('alfred-commander.panel.focus'); + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand('alfred-commander.toggle', () => { + if (provider.view) { + provider.view.webview.postMessage({ type: 'toggle-listening' }); + } else { + vscode.commands.executeCommand('alfred-commander.panel.focus'); + } + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand('alfred-commander.showStats', () => { + showStatsPanel(context); + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand('alfred-commander.welcome', () => { + vscode.commands.executeCommand('workbench.action.openWalkthrough', 'gositeme.alfred-commander#alfred-ide-getting-started', true); + }) + ); + + // --- Workspace Status Panel --- + context.subscriptions.push( + vscode.commands.registerCommand('alfred-commander.workspaceStatus', async () => { + const panel = vscode.window.createWebviewPanel( + 'alfredWorkspaceStatus', 'Workspace Status', vscode.ViewColumn.One, + { enableScripts: true } + ); + + const sessionFile = require('path').join(context.globalStorageUri?.fsPath || '', '..', '..', '..', '..', '.alfred-ide-session.json'); + let bearerToken = ''; + try { + const sData = JSON.parse(require('fs').readFileSync( + require('path').join(require('os').homedir(), '.alfred-ide-session.json'), 'utf8' + )); + bearerToken = sData.token || ''; + } catch (_) {} + + panel.webview.html = getWorkspaceStatusHTML(bearerToken); + }) + ); + + const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); + statusBar.text = '$(mic) Alfred'; + statusBar.tooltip = 'Alfred — Toggle mic (Ctrl+Shift+Alt+A)'; + statusBar.command = 'alfred-commander.toggle'; + statusBar.color = '#e2b340'; + statusBar.show(); + context.subscriptions.push(statusBar); + + const userStatus = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0); + userStatus.command = 'alfred-commander.showStats'; + context.subscriptions.push(userStatus); + + fetchUserProfile(context).then(profile => { + userProfile = profile; + userStatus.text = `$(account) Logged in as ${profile.name}`; + userStatus.tooltip = `${profile.name} (${profile.email || profile.user}) — Click for Account & Usage Stats`; + userStatus.color = '#4ec9b0'; + userStatus.show(); + if (provider.view) { + provider.view.webview.postMessage({ type: 'user-profile', profile }); + } + }).catch(() => { + const osUser = os.userInfo().username || 'gositeme'; + userStatus.text = `$(account) Logged in as ${osUser}`; + userStatus.tooltip = `Authenticated as ${osUser} — Click for Account & Usage Stats`; + userStatus.color = '#4ec9b0'; + userStatus.show(); + }); +} + +async function fetchUserProfile(context) { + const saved = context.globalState.get('alfredCommanderUserProfile'); + if (saved && saved.name && saved.fetchedAt) { + const age = Date.now() - saved.fetchedAt; + const authenticated = !!saved.client_id; + if ((authenticated && age < 3600000) || (!authenticated && age < 60000)) { + return saved; + } + } + + const ideToken = readLocalIdeToken(); + + try { + const profile = await new Promise((resolve, reject) => { + const headers = { 'X-Alfred-Source': 'ide-extension' }; + if (ideToken) { + headers['Authorization'] = 'Bearer ' + ideToken; + headers['X-Alfred-IDE-Token'] = ideToken; + } + const req = https.request({ + hostname: 'gositeme.com', port: 443, + path: '/api/alfred-ide-session.php', + method: 'GET', + headers, + timeout: 10000 + }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + const j = JSON.parse(data); + if (j.valid && j.name) resolve(j); + else reject(new Error('Invalid session')); + } catch { reject(new Error('Parse error')); } + }); + }); + req.on('error', reject); + req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); }); + req.end(); + }); + + const result = { + name: profile.name, email: profile.email, + avatar: profile.avatar || '', user: profile.email, + client_id: profile.client_id, fetchedAt: Date.now() + }; + context.globalState.update('alfredCommanderUserProfile', result); + return result; + } catch { + // Fallback: read identity from session bridge file (works without network) + for (const bridgePath of IDE_SESSION_BRIDGES) { + try { + if (!fs.existsSync(bridgePath)) continue; + const bData = JSON.parse(fs.readFileSync(bridgePath, 'utf8') || '{}'); + if (bData.client_id) { + return { name: bData.name || 'User', user: bData.email || '', email: bData.email || '', client_id: bData.client_id, fetchedAt: Date.now() }; + } + } catch (_) {} + } + const osUser = os.userInfo().username || 'gositeme'; + return { name: osUser, user: osUser, email: '', fetchedAt: Date.now() }; + } +} + +async function ensureFreshUserProfile(context) { + if (!userProfile || !userProfile.client_id || !userProfile.fetchedAt || (Date.now() - userProfile.fetchedAt) > 300000) { + userProfile = await fetchUserProfile(context); + } + return userProfile; +} + +class AlfredCommanderProvider { + constructor(extensionUri, context) { + this.extensionUri = extensionUri; + this.context = context; + this.view = null; + } + + resolveWebviewView(webviewView) { + this.view = webviewView; + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this.extensionUri] + }; + // Inject session token and identity so webview can call API directly (bypasses broken IPC) + const injToken = readLocalIdeToken(); + const injIdentity = buildIdeIdentityPayload(); + webviewView.webview.html = getWebviewContent(injToken, injIdentity || {}); + + webviewView.webview.onDidReceiveMessage(async (msg) => { + if (msg.type === 'ai-request') { + console.log('[alfred-commander] ai-request received:', msg.text && msg.text.substring(0, 80), 'agent=' + msg.agent, 'model=' + msg.model, 'id=' + msg.id); + try { + ensureFreshUserProfile(this.context).then((refreshed) => { + if (!refreshed) return; + webviewView.webview.postMessage({ type: 'user-profile', profile: refreshed }); + }).catch(() => {}); + const ctx = getEditorContext(); + console.log('[alfred-commander] Calling queryAlfredAPI...'); + const response = await queryAlfredAPI(msg.text, msg.agent || 'alfred', ctx, msg.model || 'sonnet', msg.images || [], msg.pdf_files || [], msg.attachment_texts || [], msg.zip_files || [], msg.multiplier || 30); + console.log('[alfred-commander] Got response:', response.text && response.text.substring(0, 100)); + webviewView.webview.postMessage({ type: 'ai-response', text: response.text, agent: response.agent, id: msg.id, identity: response.identity || null, attachment_report: response.attachmentReport || [] }); + generateTTS(response.text).then(audioB64 => { + if (audioB64) { + webviewView.webview.postMessage({ type: 'play-audio', audio: audioB64 }); + } + }).catch(() => {}); + } catch (err) { + console.error('[alfred-commander] ai-request ERROR:', err.message); + webviewView.webview.postMessage({ type: 'ai-response', text: 'Sorry, I had trouble processing that. ' + err.message, agent: 'alfred', id: msg.id }); + } + } else if (msg.type === 'stt-request') { + transcribeAudio(msg.audio, msg.mime).then(text => { + webviewView.webview.postMessage({ type: 'stt-result', text: text || '', id: msg.id }); + }).catch(() => { + webviewView.webview.postMessage({ type: 'stt-result', text: '', id: msg.id, error: 'Transcription failed' }); + }); + } else if (msg.type === 'ide-quick') { + const map = { + terminal: 'workbench.action.terminal.new', + save: 'workbench.action.files.save', + saveAll: 'workbench.action.files.saveAll', + palette: 'workbench.action.showCommands', + split: 'workbench.action.splitEditor', + newFile: 'workbench.action.files.newUntitledFile', + }; + const cmd = map[msg.cmd]; + if (cmd) vscode.commands.executeCommand(cmd); + } else if (msg.type === 'insert-code') { + const editor = vscode.window.activeTextEditor; + if (editor) { + editor.edit(editBuilder => { + editBuilder.insert(editor.selection.active, msg.code); + }); + } + } else if (msg.type === 'run-terminal') { + const terminal = vscode.window.createTerminal('Alfred Command'); + terminal.show(); + terminal.sendText(msg.command); + webviewView.webview.postMessage({ type: 'command-result', text: 'Running in terminal: ' + msg.command }); + } else if (msg.type === 'set-agent') { + currentAgent = msg.agent; + } else if (msg.type === 'tts-request') { + generateTTS(msg.text).then(audioB64 => { + if (audioB64) { + webviewView.webview.postMessage({ type: 'play-audio', audio: audioB64 }); + } + }).catch(() => {}); + } else if (msg.type === 'save-profile') { + if (msg.profile) { + this.context.globalState.update('alfredCommanderUserProfile', msg.profile); + userProfile = msg.profile; + } + } + }); + + if (userProfile) { + setTimeout(() => { + webviewView.webview.postMessage({ type: 'user-profile', profile: userProfile }); + }, 500); + } + } +} + +function transcribeAudio(audioB64, mime) { + if (!audioB64) return Promise.resolve(''); + + return new Promise((resolve) => { + const raw = Buffer.from(audioB64, 'base64'); + if (raw.length < 100) { resolve(''); return; } + + const ext = (mime || '').includes('wav') ? 'wav' + : (mime || '').includes('ogg') ? 'ogg' + : (mime || '').includes('mp4') || (mime || '').includes('m4a') ? 'm4a' + : 'webm'; + + const boundary = '----AlfredSTT' + Date.now(); + const filename = 'audio.' + ext; + const contentType = mime || 'audio/webm'; + + const header = Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="audio"; filename="${filename}"\r\nContent-Type: ${contentType}\r\n\r\n` + ); + const modelField = Buffer.from( + `\r\n--${boundary}\r\nContent-Disposition: form-data; name="model"\r\n\r\nwhisper-1\r\n--${boundary}--\r\n` + ); + const body = Buffer.concat([header, raw, modelField]); + + const req = https.request({ + hostname: 'gositeme.com', + port: 443, + path: '/api/stt.php', + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data; boundary=' + boundary, + 'Content-Length': body.length, + 'X-Alfred-Source': 'ide-extension' + }, + timeout: 60000 + }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + const j = JSON.parse(data); + resolve(j.text || ''); + } catch { resolve(''); } + }); + }); + req.on('error', () => resolve('')); + req.on('timeout', () => { req.destroy(); resolve(''); }); + req.write(body); + req.end(); + }); +} + +function generateTTS(text) { + if (!text || text.length < 2) return Promise.resolve(null); + const clean = text.replace(/```[\s\S]*?```/g, ' code block ') + .replace(/[*_`#~\[\]]/g, '') + .replace(/https?:\/\/\S+/g, '') + .replace(/\s+/g, ' ') + .trim() + .substring(0, 4000); + if (clean.length < 2) return Promise.resolve(null); + + return new Promise((resolve) => { + const body = JSON.stringify({ text: clean, voice: 'onyx' }); + const req = https.request({ + hostname: 'gositeme.com', + port: 443, + path: '/api/tts.php', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + 'X-Alfred-Source': 'ide-extension' + }, + timeout: 30000 + }, (res) => { + if (res.statusCode !== 200) { resolve(null); return; } + const chunks = []; + res.on('data', chunk => chunks.push(chunk)); + res.on('end', () => { + const buf = Buffer.concat(chunks); + if (buf.length < 200) { resolve(null); return; } + const contentType = res.headers['content-type'] || 'audio/mpeg'; + resolve('data:' + contentType + ';base64,' + buf.toString('base64')); + }); + }); + req.on('error', () => resolve(null)); + req.on('timeout', () => { req.destroy(); resolve(null); }); + req.write(body); + req.end(); + }); +} + +function getEditorContext() { + const editor = vscode.window.activeTextEditor; + if (!editor) return ''; + const doc = editor.document; + const sel = editor.selection; + let ctx = `[IDE Context] File: ${doc.fileName}, Language: ${doc.languageId}, Lines: ${doc.lineCount}`; + if (!sel.isEmpty) { + const selected = doc.getText(sel).substring(0, 500); + ctx += `, Selected code:\n\`\`\`${doc.languageId}\n${selected}\n\`\`\``; + } else { + const line = doc.lineAt(sel.active.line); + ctx += `, Current line ${sel.active.line + 1}: ${line.text.substring(0, 200)}`; + } + return ctx; +} + +async function queryAlfredAPI(prompt, agent, editorContext, selectedModel = 'sonnet', images = [], pdfFiles = [], attachmentTexts = [], zipFiles = [], multiplier = 30) { + const payload = { + message: prompt, agent: agent || 'alfred', + context: editorContext || '', channel: 'ide-chat', + conv_id: convId || '', model: selectedModel, + token_multiplier: multiplier || 30 + }; + const ideToken = readLocalIdeToken(); + const ideIdentity = buildIdeIdentityPayload(); + // Duplicate token in JSON body — some stacks strip Authorization / X-Alfred-IDE-Token before PHP sees them. + if (ideToken) { + payload.ide_session_token = ideToken; + } + if (ideIdentity) { + payload.ide_client_id = ideIdentity.ide_client_id; + payload.ide_name = ideIdentity.ide_name; + payload.ide_ts = ideIdentity.ide_ts; + payload.ide_sig = ideIdentity.ide_sig; + payload.ide_email = ideIdentity.ide_email; + } + if (Array.isArray(images) && images.length > 0) { + payload.images = images; + } + if (Array.isArray(pdfFiles) && pdfFiles.length > 0) { + payload.pdf_files = pdfFiles; + } + if (Array.isArray(attachmentTexts) && attachmentTexts.length > 0) { + payload.attachment_texts = attachmentTexts; + } + if (Array.isArray(zipFiles) && zipFiles.length > 0) { + payload.zip_files = zipFiles; + } + const body = JSON.stringify(payload); + + const doRequest = (csrf, cookie) => { + return new Promise((resolve, reject) => { + const headers = { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'X-Alfred-Source': 'alfred-ide' }; + if (csrf) headers['X-CSRF-Token'] = csrf; + if (cookie) headers['Cookie'] = cookie; + if (ideToken) { + headers['Authorization'] = 'Bearer ' + ideToken; + // Apache/CGI often strips Authorization before PHP — duplicate token for alfred_resolve_ide_bearer_token() + headers['X-Alfred-IDE-Token'] = ideToken; + } + const req = https.request({ hostname: 'gositeme.com', port: 443, path: '/api/alfred-chat.php', method: 'POST', headers, timeout: 120000 }, (res) => { + // Node may give Set-Cookie as a string OR an array — iterating a string walks each CHARACTER and breaks PHPSESSID capture. + const setCookie = res.headers['set-cookie']; + if (setCookie) { + const parts = Array.isArray(setCookie) ? setCookie : [setCookie]; + for (let i = 0; i < parts.length; i++) { + const m = String(parts[i]).match(/PHPSESSID=([^;]+)/); + if (m) { + sessionCookie = 'PHPSESSID=' + m[1]; + break; + } + } + } + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { console.error('[alfred-commander] Non-JSON response HTTP ' + res.statusCode + ':', data.substring(0, 600)); resolve({ response: data.trim() || 'Alfred returned an empty response. Please send your message again.' }); } }); + }); + req.on('error', (err) => reject(new Error('API failed: ' + err.message))); + req.on('timeout', () => { req.destroy(); reject(new Error('Timed out (120s)')); }); + req.write(body); + req.end(); + }); + }; + let result = await doRequest(csrfToken, sessionCookie); + if (result.csrf_token) csrfToken = result.csrf_token; + for (let csrfAttempt = 0; csrfAttempt < 4 && result && (result.csrf_refresh || result.response === 'Session initialized. Please retry.' || result.error === 'CSRF validation failed'); csrfAttempt++) { + result = await doRequest(csrfToken, sessionCookie); + if (result.csrf_token) csrfToken = result.csrf_token; + } + if (!result || (!result.response && !result.message && !result.error)) { + logTelemetry('empty-response: retrying once'); + result = await doRequest(csrfToken, sessionCookie); + if (result && result.csrf_token) csrfToken = result.csrf_token; + } + if (result.conv_id) convId = result.conv_id; + const errText = result && result.error ? String(result.error) : ''; + const textOut = (result && (result.response || result.message)) + ? (result.response || result.message) + : (errText ? ('⚠️ ' + errText) : 'No response from AI'); + return { + text: textOut, + agent: (result && result.agent) ? result.agent : agent, + identity: result && result.identity ? result.identity : null, + attachmentReport: (result && result.attachment_report) ? result.attachment_report : [] + }; +} + +let statsPanel = null; + +function showStatsPanel(context) { + if (statsPanel) { + statsPanel.reveal(vscode.ViewColumn.One); + return; + } + + statsPanel = vscode.window.createWebviewPanel( + 'alfredStats', + 'Account & Usage Stats', + vscode.ViewColumn.One, + { enableScripts: true, retainContextWhenHidden: false } + ); + + statsPanel.onDidDispose(() => { statsPanel = null; }); + + const ideToken = readLocalIdeToken(); + const headers = { 'X-Alfred-Source': 'ide-extension' }; + if (ideToken) { + headers['Authorization'] = 'Bearer ' + ideToken; + headers['X-Alfred-IDE-Token'] = ideToken; + } + + const req = https.request({ + hostname: 'gositeme.com', port: 443, + path: '/api/alfred-ide-session.php?action=stats', + method: 'GET', headers, timeout: 15000 + }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + const stats = JSON.parse(data); + if (statsPanel) { + statsPanel.webview.html = getStatsHtml(stats); + } + } catch (e) { + if (statsPanel) { + statsPanel.webview.html = getStatsHtml({ valid: false, error: 'Failed to load stats: ' + e.message }); + } + } + }); + }); + req.on('error', (e) => { + if (statsPanel) { + statsPanel.webview.html = getStatsHtml({ valid: false, error: 'Connection error: ' + e.message }); + } + }); + req.on('timeout', () => { req.destroy(); }); + req.end(); + + // Show loading state immediately + statsPanel.webview.html = getStatsHtml(null); +} + +function getStatsHtml(stats) { + if (!stats) { + return ` + +

Loading your stats...

`; + } + + if (!stats.valid) { + return ` + +

Account Stats

` + escHtml(stats.error || 'Unable to load stats') + `

`; + } + + const name = escHtml(stats.name || 'User'); + const email = escHtml(stats.email || ''); + const plan = (stats.plan || 'free').toLowerCase(); + const planDisplay = plan === 'commander' ? 'Commander' : (stats.plan || 'Free').charAt(0).toUpperCase() + (stats.plan || 'free').slice(1); + const planColors = { commander: '#e2b340', free: '#6a737d', starter: '#3b82f6', professional: '#a855f7', enterprise: '#22c55e' }; + const planColor = planColors[plan] || '#6a737d'; + + const tokensUsed = stats.tokens_used || 0; + const tokensIncluded = stats.tokens_included || 50000; + const tokensOverage = stats.tokens_overage || 0; + const costOverage = stats.cost_overage_usd || 0; + const isUnlimited = stats.unlimited || plan === 'commander'; + const remaining = Math.max(0, tokensIncluded - tokensUsed); + const pct = tokensIncluded > 0 ? Math.min(100, Math.round((tokensUsed / tokensIncluded) * 100)) : 0; + const barColor = pct >= 90 ? '#ef4444' : pct >= 70 ? '#f59e0b' : '#3b82f6'; + + const fmtK = (n) => n >= 1000000 ? (n / 1000000).toFixed(1) + 'M' : n >= 1000 ? Math.round(n / 1000) + 'K' : String(n); + + // Services section + let servicesHtml = ''; + if (stats.services && stats.services.length > 0) { + servicesHtml = stats.services.map(s => + `${escHtml(s.product || 'Service')}${escHtml(s.domain || '—')}${escHtml(s.status)}$${parseFloat(s.amount || 0).toFixed(2)}/${escHtml(s.billing_cycle || 'N/A')}` + ).join(''); + } else { + servicesHtml = 'No active services'; + } + + // Recent invoices + let invoicesHtml = ''; + if (stats.recent_invoices && stats.recent_invoices.length > 0) { + invoicesHtml = stats.recent_invoices.map(i => { + const statusClass = i.status === 'Paid' ? 'status-active' : i.status === 'Unpaid' ? 'status-unpaid' : 'status-other'; + return `#${escHtml(i.invoice_number || String(i.id))}$${parseFloat(i.total || 0).toFixed(2)}${escHtml(i.status)}${escHtml((i.due_date || i.created_at || '').substring(0, 10))}`; + }).join(''); + } else { + invoicesHtml = 'No invoices'; + } + + // Usage breakdown + let usageBreakdownHtml = ''; + if (stats.usage_breakdown && stats.usage_breakdown.length > 0) { + usageBreakdownHtml = stats.usage_breakdown.map(u => + `${escHtml(u.feature)}${escHtml(u.model)}${fmtK(parseInt(u.input_tokens || 0) + parseInt(u.output_tokens || 0))}${parseInt(u.requests || 0)}$${parseFloat(u.cost_usd || 0).toFixed(4)}` + ).join(''); + } else { + usageBreakdownHtml = 'No usage this period'; + } + + return ` + + + + + + + +

$(account) ${name}

+
${email} · Last login: ${escHtml((stats.last_login || '').substring(0, 16))}
+ +
+
+
Plan
+
${planDisplay}
+
+
+
Credit Balance
+
$${escHtml(stats.credit_balance || '0.00')}
+
+
+
Active Services
+
${stats.active_services || 0}
+
+
+
Unpaid Invoices
+
${stats.unpaid_invoices || 0}
+
${(stats.unpaid_invoices || 0) > 0 ? 'Total due: $' + escHtml(stats.total_due || '0.00') : 'All clear'}
+
+
+ +
+
+ Token Usage — ${new Date().toLocaleString('en-US', { month: 'long', year: 'numeric' })} + ${planDisplay} +
+ ${isUnlimited + ? `
+
Unlimited usage — Commander plan
` + : `
+
${fmtK(tokensUsed)} used of ${fmtK(tokensIncluded)} included (${fmtK(remaining)} remaining)${tokensOverage > 0 ? ' · Overage: ' + fmtK(tokensOverage) + ' tokens ($' + costOverage.toFixed(2) + ')' : ''}
` + } +
+ +
Usage Breakdown
+ + + ${usageBreakdownHtml} +
FeatureModelTokensRequestsCost
+ +
Active Services
+ + + ${servicesHtml} +
ProductDomainStatusAmount
+ +
Recent Invoices
+ + + ${invoicesHtml} +
InvoiceAmountStatusDate
+ + + +`; +} + +function escHtml(str) { + return String(str || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +function getWebviewContent(injectedToken, injectedIdentity) { + const safeToken = (injectedToken || '').replace(/[^a-f0-9]/g, ''); + const safeIdentity = JSON.stringify(injectedIdentity || {}); + return ` + + + + + + + + +
+

Alfred

+
All models · Voice STT/TTS · Attachments · Ctrl+Shift+Alt+A · Enter sends
+ +
+ Plan · Loading... +
+
+
+
+ + + + + + +
+
+ IDE + + + + + + +
+
+
+
Alfred
+
Ready. Claude Sonnet 4 connected. Speak or type.
+
+
+
+
+
+ +
+ + + + +
+
+
Payload: 0 KB +
+
+
+
Ready
+ + +
+ + + +`; +} + +function deactivate() {} + +function getWorkspaceStatusHTML(token) { + return ` + + + + +

🖥 Workspace Status

+
+

Loading workspace data...

+ +`; +} + +module.exports = { activate, deactivate }; diff --git a/extensions.json b/extensions.json new file mode 100644 index 0000000..32ecc08 --- /dev/null +++ b/extensions.json @@ -0,0 +1,12 @@ +{ + "recommendations": [ + "gositeme.alfred-commander" + ], + "unwantedRecommendations": [ + "ms-vscode-remote.remote-ssh", + "ms-vscode.remote-server", + "ms-vscode.remote-explorer", + "github.copilot", + "github.copilot-chat" + ] +} diff --git a/media/alfred-icon.svg b/media/alfred-icon.svg new file mode 100644 index 0000000..62c2921 --- /dev/null +++ b/media/alfred-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/media/walkthrough/meet-alfred.md b/media/walkthrough/meet-alfred.md new file mode 100644 index 0000000..804ad39 --- /dev/null +++ b/media/walkthrough/meet-alfred.md @@ -0,0 +1,17 @@ +# Meet Alfred + +Alfred is your AI coding companion, built into the sidebar. + +## What Alfred Can Do +- **Answer questions** about your code, frameworks, and tools +- **Generate code** from natural language descriptions +- **Debug issues** by analyzing errors and suggesting fixes +- **Explain code** — select any block and ask "what does this do?" +- **Run tools** — file search, terminal commands, web lookups + +## How to Start +1. Click the **Alfred icon** (🅰️) in the activity bar (left side) +2. Type your question or request in the chat input +3. Press Enter — Alfred responds in real time + +Alfred remembers your conversation context, so you can have natural multi-turn discussions about your code. diff --git a/media/walkthrough/models.md b/media/walkthrough/models.md new file mode 100644 index 0000000..054ff49 --- /dev/null +++ b/media/walkthrough/models.md @@ -0,0 +1,18 @@ +# AI Models + +Alfred IDE supports multiple AI providers and models. + +## Available Tiers + +| Tier | Best For | Examples | +|------|----------|---------| +| **Leaf** | Simple tasks, quick answers | GPT-4o-mini, local Ollama | +| **Mid** | Code generation, analysis | Claude 3.5 Sonnet, GPT-4o | +| **Frontier** | Complex reasoning, architecture | Claude Opus, GPT-4 | +| **Max** | Research, multi-step planning | Claude Opus (extended) | + +## How to Switch +Use the **model dropdown** at the top of the Alfred panel to select your preferred model. + +## Local Models +If Ollama is running, local models appear automatically — your code never leaves your machine. diff --git a/media/walkthrough/shortcuts.md b/media/walkthrough/shortcuts.md new file mode 100644 index 0000000..b7cca88 --- /dev/null +++ b/media/walkthrough/shortcuts.md @@ -0,0 +1,24 @@ +# Keyboard Shortcuts + +Master these to work at full speed. + +## Alfred Commands +| Shortcut | Action | +|----------|--------| +| `Ctrl+Shift+Alt+O` | Open Alfred Panel | +| `Ctrl+Shift+Alt+A` | Toggle Voice Mode | + +## Essential IDE Shortcuts +| Shortcut | Action | +|----------|--------| +| `Ctrl+P` | Quick File Open | +| `Ctrl+Shift+P` | Command Palette | +| `Ctrl+`` ` | Toggle Terminal | +| `Ctrl+B` | Toggle Sidebar | +| `Ctrl+Shift+E` | File Explorer | +| `Ctrl+Shift+F` | Search Across Files | +| `Ctrl+Shift+G` | Source Control | +| `F5` | Start Debugging | +| `Ctrl+/` | Toggle Line Comment | +| `Alt+↑/↓` | Move Line Up/Down | +| `Ctrl+D` | Select Next Occurrence | diff --git a/media/walkthrough/sovereign.md b/media/walkthrough/sovereign.md new file mode 100644 index 0000000..65e7edb --- /dev/null +++ b/media/walkthrough/sovereign.md @@ -0,0 +1,27 @@ +# Sovereign Development + +Alfred IDE is built on one principle: **your code is yours**. + +## What We Don't Do +- ❌ No telemetry or usage tracking +- ❌ No analytics or behavior profiling +- ❌ No data sold to third parties +- ❌ No phone-home to Microsoft, Google, or anyone +- ❌ No extension marketplace surveillance + +## What We Do +- ✅ All AI calls go through your GoSiteMe account +- ✅ Conversations are stored in YOUR database +- ✅ Local Ollama models keep everything on-device +- ✅ Secret redaction engine scrubs credentials from all output +- ✅ Post-quantum encryption available via Veil Protocol + +## The GoSiteMe Ecosystem +Alfred IDE is one pillar of the sovereign computing stack: +- **Alfred Linux** — AI-native operating system +- **Alfred Browser** — zero-tracking web browser +- **Alfred Mobile** — sovereign smartphone OS +- **Veil** — post-quantum encrypted messaging +- **MetaDome** — VR worlds with 51M+ AI agents + +Welcome to the kingdom. 🏰 diff --git a/media/walkthrough/stats.md b/media/walkthrough/stats.md new file mode 100644 index 0000000..b35bc60 --- /dev/null +++ b/media/walkthrough/stats.md @@ -0,0 +1,16 @@ +# Account & Usage + +Monitor your plan, usage, and billing from inside the IDE. + +## How to Access +Click your **username** in the bottom status bar, or run: +- Command Palette → "Alfred: Account & Usage Stats" + +## What You'll See +- **Plan**: Your current subscription tier +- **Token Usage**: How many AI tokens you've consumed +- **Credit Balance**: Remaining credits for the billing period +- **Active Services**: Which ecosystem services are linked + +## Commander Access +If you're the Commander (client_id 33), you have unlimited access — no token limits, no restrictions. diff --git a/media/walkthrough/voice.md b/media/walkthrough/voice.md new file mode 100644 index 0000000..c2ca72b --- /dev/null +++ b/media/walkthrough/voice.md @@ -0,0 +1,18 @@ +# Voice Commands + +Talk to your IDE — Alfred listens, transcribes, and responds. + +## How It Works +1. Click the **🎤 microphone** button in the Alfred panel +2. Speak naturally — your voice is transcribed in real time +3. Alfred processes your request and responds + +## Voice Architecture +- **STT (Speech-to-Text)**: Whisper-based transcription +- **LLM**: Your selected AI model processes the request +- **TTS (Text-to-Speech)**: Kokoro voices read the response back + +## Tips +- Speak clearly and at a normal pace +- You can say "stop" or click the mic again to end recording +- Voice works best with short, focused requests diff --git a/package.json b/package.json new file mode 100644 index 0000000..af99615 --- /dev/null +++ b/package.json @@ -0,0 +1,137 @@ +{ + "name": "alfred-commander", + "displayName": "Alfred IDE Assistant — Full IDE Chat & Voice", + "description": "Fresh Alfred panel: all agents & models, voice STT/TTS, attachments, hands-free. IDE shortcuts are explicit buttons — chat text always goes to AI.", + "version": "1.0.1", + "publisher": "gositeme", + "engines": { + "vscode": "^1.70.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./extension.js", + "contributes": { + "configurationDefaults": { + "window.menuBarVisibility": "visible" + }, + "commands": [ + { + "command": "alfred-commander.open", + "title": "Alfred: Open Panel" + }, + { + "command": "alfred-commander.toggle", + "title": "Alfred: Toggle Mic" + }, + { + "command": "alfred-commander.showStats", + "title": "Alfred: Account & Usage Stats" + }, + { + "command": "alfred-commander.welcome", + "title": "Alfred: Getting Started" + }, + { + "command": "alfred-commander.workspaceStatus", + "title": "Alfred: Workspace Status" + } + ], + "keybindings": [ + { + "command": "alfred-commander.open", + "key": "ctrl+shift+alt+o", + "mac": "cmd+shift+alt+o" + }, + { + "command": "alfred-commander.toggle", + "key": "ctrl+shift+alt+a", + "mac": "cmd+shift+alt+a" + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "alfred-commander-container", + "title": "Alfred", + "icon": "media/alfred-icon.svg" + } + ] + }, + "views": { + "alfred-commander-container": [ + { + "type": "webview", + "id": "alfred-commander.panel", + "name": "Alfred", + "visibility": "visible" + } + ] + }, + "walkthroughs": [ + { + "id": "alfred-ide-getting-started", + "title": "Getting Started with Alfred IDE", + "description": "Your sovereign development environment — AI-powered, zero-tracking, fully yours.", + "steps": [ + { + "id": "meet-alfred", + "title": "Meet Alfred — Your AI Companion", + "description": "Alfred lives in the sidebar. Click the Alfred icon in the activity bar to open the chat panel. Ask anything — code questions, file operations, debugging help.\n\n[Open Alfred Panel](command:alfred-commander.open)", + "media": { + "markdown": "media/walkthrough/meet-alfred.md" + } + }, + { + "id": "voice-commands", + "title": "Talk to Your IDE", + "description": "Alfred supports voice input. Click the microphone button or press ``Ctrl+Shift+Alt+A`` to toggle voice mode. Speak naturally — Alfred transcribes and responds.\n\n[Toggle Voice](command:alfred-commander.toggle)", + "media": { + "markdown": "media/walkthrough/voice.md" + } + }, + { + "id": "choose-model", + "title": "Choose Your AI Model", + "description": "Switch between AI providers and models using the dropdown at the top of the Alfred panel. Available models include Claude, GPT, local Ollama models, and more.", + "media": { + "markdown": "media/walkthrough/models.md" + } + }, + { + "id": "account-stats", + "title": "Check Your Usage & Plan", + "description": "Click your username in the status bar (bottom-left) to see your account details, token usage, plan info, and billing.\n\n[View Account Stats](command:alfred-commander.showStats)", + "media": { + "markdown": "media/walkthrough/stats.md" + } + }, + { + "id": "keyboard-shortcuts", + "title": "Essential Shortcuts", + "description": "Master these shortcuts to work faster:\n- ``Ctrl+Shift+Alt+O`` — Open Alfred Panel\n- ``Ctrl+Shift+Alt+A`` — Toggle Voice\n- ``Ctrl+``` `` — Open Terminal\n- ``Ctrl+P`` — Quick File Open\n- ``Ctrl+Shift+P`` — Command Palette", + "media": { + "markdown": "media/walkthrough/shortcuts.md" + } + }, + { + "id": "sovereign-ide", + "title": "Your Sovereign IDE", + "description": "Alfred IDE tracks nothing. No telemetry, no usage analytics, no data collection. Your code stays on your machine. Your conversations stay private. Welcome to sovereign development.", + "media": { + "markdown": "media/walkthrough/sovereign.md" + } + } + ] + } + ] + }, + "__metadata": { + "installedTimestamp": 1774281381634, + "targetPlatform": "undefined", + "size": 75665 + } +} \ No newline at end of file