From 6b064f9a758515d523b11bcd57ac99a20193a4a7 Mon Sep 17 00:00:00 2001 From: Solar Lorian Date: Mon, 4 May 2026 00:42:35 +0100 Subject: [PATCH] feat: Kilo, Cline, Antigravity IDE, project isolation, launch panel --- Cargo.lock | 2 +- antigravity-integration.md | 340 ++++++++++ extensions/hcom-bridge/.gitignore | 2 + extensions/hcom-bridge/.vscodeignore | 6 + extensions/hcom-bridge/build.sh | 22 + extensions/hcom-bridge/package.json | 70 ++ extensions/hcom-bridge/src/antigravity.d.ts | 10 + extensions/hcom-bridge/src/extension.ts | 67 ++ extensions/hcom-bridge/src/hcomClient.ts | 301 +++++++++ extensions/hcom-bridge/src/jetskiBridge.ts | 245 +++++++ extensions/hcom-bridge/tsconfig.json | 18 + src/agent_prompts.rs | 47 ++ src/antigravity_extension/extension.js | 21 + src/antigravity_extension/package.json | 70 ++ src/bootstrap.rs | 56 +- src/cli_context.rs | 15 +- src/commands/config.rs | 38 +- src/commands/help.rs | 68 +- src/commands/hooks.rs | 73 ++- src/commands/launch.rs | 29 +- src/commands/list.rs | 23 + src/commands/resume.rs | 44 +- src/commands/send.rs | 71 +-- src/commands/start.rs | 2 + src/commands/status.rs | 52 ++ src/commands/transcript.rs | 1 + src/config.rs | 38 ++ src/core/bundles.rs | 2 + src/core/helpers.rs | 3 + src/db.rs | 127 ++-- src/delivery.rs | 16 +- src/hooks/antigravity.rs | 185 ++++++ src/hooks/cline.rs | 618 ++++++++++++++++++ src/hooks/family.rs | 6 + src/hooks/kilo.rs | 666 ++++++++++++++++++++ src/hooks/mod.rs | 3 + src/hooks/opencode.rs | 15 +- src/identity.rs | 22 +- src/instance_binding.rs | 17 +- src/instance_lifecycle.rs | 105 ++- src/instances.rs | 6 +- src/launcher.rs | 147 ++++- src/main.rs | 16 + src/messages.rs | 43 +- src/opencode_plugin/TaskCancel | 15 + src/opencode_plugin/TaskComplete | 15 + src/opencode_plugin/TaskResume | 20 + src/opencode_plugin/TaskStart | 19 + src/opencode_plugin/UserPromptSubmit | 18 + src/opencode_plugin/clinehook.sh | 19 + src/opencode_plugin/hcom.ts | 12 +- src/opencode_plugin/hcom_kilo.ts | 506 +++++++++++++++ src/opencode_plugin/kilo-hcom.ts | 496 +++++++++++++++ src/pty/mod.rs | 2 +- src/pty/screen.rs | 2 + src/relay/control.rs | 3 + src/router.rs | 9 +- src/shared/constants.rs | 6 +- src/shared/identity.rs | 45 ++ src/shared/platform.rs | 2 + src/terminal.rs | 7 +- src/tool.rs | 61 +- src/tools/cline_preprocessing.rs | 44 ++ src/tools/kilo_preprocessing.rs | 47 ++ src/tools/kilocode_preprocessing.rs | 4 + src/tools/mod.rs | 2 + src/tui/actions.rs | 14 + src/tui/app.rs | 4 + src/tui/db.rs | 40 +- src/tui/input.rs | 66 +- src/tui/mod.rs | 1 + src/tui/model.rs | 83 ++- src/tui/render/agents.rs | 64 +- src/tui/render/launch.rs | 53 +- src/tui/render/mod.rs | 28 +- src/tui/rpc.rs | 19 +- src/tui/rpc_async.rs | 16 +- src/tui/state.rs | 2 + src/tui/status.rs | 1 + tests/test_pty_delivery.rs | 293 ++++++++- 80 files changed, 5491 insertions(+), 275 deletions(-) create mode 100644 antigravity-integration.md create mode 100644 extensions/hcom-bridge/.gitignore create mode 100644 extensions/hcom-bridge/.vscodeignore create mode 100644 extensions/hcom-bridge/build.sh create mode 100644 extensions/hcom-bridge/package.json create mode 100644 extensions/hcom-bridge/src/antigravity.d.ts create mode 100644 extensions/hcom-bridge/src/extension.ts create mode 100644 extensions/hcom-bridge/src/hcomClient.ts create mode 100644 extensions/hcom-bridge/src/jetskiBridge.ts create mode 100644 extensions/hcom-bridge/tsconfig.json create mode 100644 src/agent_prompts.rs create mode 100644 src/antigravity_extension/extension.js create mode 100644 src/antigravity_extension/package.json create mode 100644 src/hooks/antigravity.rs create mode 100644 src/hooks/cline.rs create mode 100644 src/hooks/kilo.rs create mode 100755 src/opencode_plugin/TaskCancel create mode 100755 src/opencode_plugin/TaskComplete create mode 100755 src/opencode_plugin/TaskResume create mode 100755 src/opencode_plugin/TaskStart create mode 100755 src/opencode_plugin/UserPromptSubmit create mode 100755 src/opencode_plugin/clinehook.sh create mode 100644 src/opencode_plugin/hcom_kilo.ts create mode 100644 src/opencode_plugin/kilo-hcom.ts create mode 100644 src/tools/cline_preprocessing.rs create mode 100644 src/tools/kilo_preprocessing.rs create mode 100644 src/tools/kilocode_preprocessing.rs diff --git a/Cargo.lock b/Cargo.lock index df48682c..91e403af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,7 +855,7 @@ dependencies = [ [[package]] name = "hcom" -version = "0.7.14" +version = "0.7.16" dependencies = [ "anyhow", "base64", diff --git a/antigravity-integration.md b/antigravity-integration.md new file mode 100644 index 00000000..47cf89ce --- /dev/null +++ b/antigravity-integration.md @@ -0,0 +1,340 @@ +# Antigravity + Gemini CLI — hcom Integration Analysis + +## Overview + +Two Google products were investigated for hcom integration, plus a third community +project that shares the name. Their integration potential differs significantly. + +--- + +## 1. Antigravity (Google's VS Code fork) + +**Identity:** Google-branded VS Code fork, Electron IDE (v1.107.0) +**Binary:** `antigravity` (`/usr/bin/antigravity → /opt/Antigravity/bin/antigravity`) +**Runtime:** Electron (Node.js v22.20 + Chromium) +**Package:** `apt antigravity` +**Config:** `~/.gemini/antigravity/` (mcp_config.json, conversations/, brain/) +**Alias:** `agy` +**Data folder:** `~/.antigravity/` + +### CLI interface + +```bash +antigravity chat [options] [prompt] # Open AI chat in GUI (modes: ask|edit|agent) +antigravity chat -m agent - # Pipe stdin, but STILL opens GUI window +antigravity serve-web # Web UI server (no AI chat there) +antigravity tunnel # Secure tunnel to vscode.dev +``` + +### `antigravity chat` flags + +| Flag | Description | +|------|-------------| +| `-m --mode ` | Chat mode (default: agent) | +| `-a --add-file ` | Add files as context | +| `--maximize` | Maximize chat view | +| `-r --reuse-window` | Reuse existing window | +| `-n --new-window` | Open new empty window | +| `--profile ` | Use specific profile | + +**Pipe mode** exists (`antigravity chat prompt -`) but **still opens a GUI window** +— it reads stdin into a temp file and shows it in the chat panel. + +### Probed capabilities + +| Capability | Result | +|------------|--------| +| Headless AI (no GUI) | ❌ `chat` always opens Electron window | +| Pipe mode (`-p`/stdin) | ⚠️ Reads stdin but still shows GUI | +| JSON output | ❌ No `--json` or `--output-format` | +| Hook scripts (`--hooks-dir`) | ❌ Not available | +| ACP protocol (`--acp`) | ❌ Not available | +| Auto-approve (`--yolo`) | ❌ Not available | +| Programmatic Agent API | ❌ No equivalent to ClineAgent | +| MCP server registration | ✅ `--add-mcp ` + `mcp_config.json` | +| VS Code extensions | ✅ Standard VS Code extension system | +| Transient mode | ✅ `--transient` (temp data/ext dirs) | +| Web server mode | ✅ `serve-web` but no AI chat there | +| Wait-for-completion | ❌ `--wait` not supported by `chat` subcommand | +| Listen/notify | ❌ None | + +### Viable integration approaches (ranked by effort) + +#### A. MCP-based (lowest effort, limited) + +Register hcom as an MCP server in `~/.gemini/antigravity/mcp_config.json`: + +```json +{ + "hcom": { + "command": "hcom", + "args": ["mcp", "--serve"], + "env": { "HCOM_PROJECT": "..." } + } +} +``` + +This gives Antigravity access to hcom **tools** (send, list, events, etc.) via +MCP tool calls. Good for: sending messages to hcom agents from within Antigravity. +Does NOT enable: spawning Antigravity as an hcom agent, lifecycle management, +bidirectional message delivery. + +#### B. VS Code extension (medium effort, bidirectional) + +Write a VS Code extension for Antigravity that bridges to hcom: +- Listens for Antigravity session events +- Forwards messages between hcom and Antigravity chat +- Registers commands like "Send to hcom" + +The Antigravity extension (`extensions/antigravity/`) powers all AI features +and uses `enabledApiProposals` including `antigravityUnifiedStateSync` — there +IS a proposed API for state sync that could be leveraged. + +#### C. serve-web + automation (high effort, fragile) + +`antigravity serve-web` starts a web server. Could be automated with browser +automation (Playwright/Puppeteer) to interact with the AI chat. Very fragile. + +#### D. PTY spawning (not feasible) + +`antigravity chat --mode agent --new-window` opens a GUI. Even with +`--new-window --transient`, it spawns a full Electron process. The PTY wrapper +would just see Electron output, not the AI agent's responses. + +### New finding: antigravity-cli (file-based async tasks) + +Repo: github.com/michaelw9999/antigravity-cli (13★, Python) + +This unofficial CLI reads/writes Antigravity's task artifacts in +`~/.gemini/antigravity/brain//`. It allows **file-based asynchronous** +task delegation: + +```bash +# Create a task → Jetski (Antigravity's AI) picks it up +antigravity-cli new-task "Implement feature X" + +# Read task progress +antigravity-cli show-task + +# Inject context back +antigravity-cli write-artifact --artifacttype implementation_plan \ + --primarytask --filepath ./context.md --summary "hcom message" + +# Update task status +antigravity-cli update-task --primarytask --subtask 0 --state completed +``` + +This is **async file-based** communication, not real-time bidirectional +messaging. But it DOES enable hcom to delegate work to Antigravity's AI agent +(Jetski) by creating tasks in its brain directory. + +## 4. VS Code Extension for hcom ↔ Antigravity Bridge + +**Veredicto: SÍ es posible.** Una extensión VS Code puede integrar hcom con +Antigravity usando APIs estándar y del propio Antigravity. + +### APIs disponibles para la extensión + +| API | Capacidad | Documentada? | +|-----|-----------|-------------| +| `antigravityExtensibility.sendToAgentPanel({message, files, autoSend})` | Enviar mensajes al agente Jetski | ✅ Pública y estable | +| `vscode.lm.selectChatModels()` + `sendRequest()` | Enviar prompts al LM y recibir streaming responses | ✅ VS Code API estándar | +| `vscode.chat.createChatParticipant("hcom", handler)` | Crear participante `@hcom` en el chat | ✅ VS Code API estándar | +| `vscode.lm.registerLanguageModelChatProvider("hcom", provider)` | Registrar hcom como proveedor de LM | ✅ VS Code API estándar | +| `vscode.lm.registerTool()` | Registrar tools para tool-calling | ✅ VS Code API estándar | +| `child_process.spawn()` | Spawnear hcom CLI como child process | ✅ Extension host | +| `vscode.commands.executeCommand("antigravity.*")` | Comandos internos (readTerminal, etc.) | ⚠️ No documentados | + +### Arquitectura propuesta + +``` +┌─────────────────────────────────────────────────┐ +│ Antigravity (Electron) │ +│ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ Extension: hcom-bridge │ │ +│ │ │ │ +│ │ spawns ──► hcom listen (child proc) │ │ +│ │ │ │ │ +│ │ ├► sendToAgentPanel() → Jetski │ │ +│ │ │ (mensajes entrantes de hcom) │ │ +│ │ │ │ │ +│ │ ├► ChatParticipant("@hcom") │ │ +│ │ │ (mensajes salientes a hcom) │ │ +│ │ │ │ │ +│ │ └► LM.sendRequest() → lee respuestas │ │ +│ │ (streaming de Jetski a hcom) │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ Jetski Agent (language_server_linux_x64) │ │ +│ │ Loop conversando con el LLM, tools, etc │ │ +│ └─────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────┘ + ▲ + │ hcom send @name ... + ▼ +┌─────────────────────────────────────────────────┐ +│ Otros agentes hcom (moto, sora, yuri, etc) │ +└─────────────────────────────────────────────────┘ +``` + +### Flujo bidireccional + +**hcom → Antigravity (mensaje entrante):** +1. La extensión corre `hcom listen` como child process +2. Detecta nuevo mensaje via stdout/notify +3. Llama `sendToAgentPanel({message: "...", autoSend: true})` +4. Jetski recibe el mensaje en el panel de chat y lo procesa + +**Antigravity → hcom (mensaje saliente):** +1. Usuario escribe `@hcom enviar mensaje...` en el chat de Antigravity +2. `ChatParticipant("hcom")` recibe el `ChatRequest` +3. La extensión ejecuta `hcom send @agent -- "mensaje"` +4. El mensaje se entrega al agente hcom destino + +### Consideraciones + +- **No se puede spawnear Antigravity como agente** — Antigravity es un IDE + Electron, no un CLI headless. La extensión corre DENTRO de Antigravity. +- **No hay API para recibir responses de `sendToAgentPanel`** — es fire-and-forget. + Para recibir respuestas, usar `vscode.lm.sendRequest()` al LM subyacente. +- **El agente Jetski es un binary externo** (`language_server_linux_x64`) que la + extensión principal de Antigravity gestiona. No hay API pública para + controlarlo directamente. +- **La extensión se publica en open-vsx.org** (el marketplace de Antigravity) + y se instala con `antigravity --install-extension hcom-bridge`. + +### Veredicto final + +| Approach | Tiempo real? | Bidireccional? | Esfuerzo | +|----------|-------------|---------------|----------| +| MCP server | ✅ sync | ⚠️ Solo tools (Antigravity→hcom) | Bajo | +| antigravity-cli (file tasks) | ❌ async | ✅ Ambos sentidos | Bajo | +| **VS Code extension** | ✅ **sync** | **✅ Completo** | **Medio** | + +La extensión VS Code es la **única forma de lograr integración completa en +tiempo real** entre hcom y Antigravity. Las otras opciones (MCP, antigravity-cli) +son parciales o asíncronas. + +--- + +## 2. Gemini CLI (`gemini`) — Google's actual AI coding CLI + +**Identity:** Google's official AI coding CLI (like Claude Code) +**Binary:** `gemini` +**Install:** `npm install -g @google/gemini-cli` | `brew install gemini-cli` +**Runtime:** Node.js (TypeScript, open source) +**GitHub:** github.com/google-gemini/gemini-cli — 103k stars +**Config:** `~/.gemini/settings.json` +**License:** Apache 2.0 +**Free tier:** 60 req/min, 1000 req/day + +### CLI flags + +| Flag | Description | +|------|-------------| +| `gemini -p "prompt"` | Non-interactive mode (like Claude -p) | +| `--output-format json` | JSON structured output | +| `--output-format stream-json` | Streaming JSON output | +| `--include-directories` | Multi-directory context | +| `-m ` | Model selection | +| `gemini task "prompt"` | Task mode | + +### Integration assessment + +| Capability | Status | +|------------|--------| +| Headless/pipe mode | ✅ `gemini -p` | +| JSON output | ✅ `--output-format json` | +| MCP support | ✅ (in settings.json) | +| Hook system | ⚠️ Needs repo clone to confirm | +| Programmatic API | ⚠️ Needs repo clone | +| Auto-approve | ❓ Unknown (likely --yes flag) | + +### Viable approach: CLI spawning + MCP + +```bash +hcom 1 gemini -- "Write a parser for X" +``` + +Two sub-approaches: +1. **PTY spawning** (like OpenCode/Kilo/Cline) — spawn in terminal, inject via PTY +2. **Pipe mode** (like `claude -p`) — use `gemini -p` with `--output-format json` + +Needs further research: hook system, lifecycle events, extension API. + +--- + +## 3. antigravity-workspace-template (community project) + +**Author:** study8677 +**Repos:** github.com/study8677/antigravity-workspace-template (1.2k★) +**Language:** Python +**Description:** Multi-agent knowledge engine. Has own CLI (`ag init`, `ag refresh`, +`ag ask`, `ag-mcp`). Runs as MCP server. Compatible with Claude Code, Codex, +Cursor, Windsurf, Gemini CLI. + +**Not relevant for hcom integration** — this is a separate community project, +not Google's Antigravity. + +--- + +## Feasibility Summary + +| Tool | Spawn as agent? | Bidirectional msgs? | Lifecycle mgmt? | Effort | +|------|----------------|-------------------|-----------------|--------| +| **Antigravity (IDE)** | ❌ No headless AI | ⚠️ MCP (tools→hcom) / ✅ antigravity-cli (async) | ❌ | Low (MCP+cli) | +| **Gemini CLI** | ✅ Already integrated | ✅ | ✅ | Already done | +| **antigravity-workspace-template** | ❌ MCP server only | — | — | Low (MCP add) | + +## Recommended approach + +**Antigravity como MCP client** (ya funcional): + +```bash +hcom mcp --serve --project X +# Then register in Antigravity: +antigravity --add-mcp '{"name":"hcom","command":"hcom","args":["mcp","--serve","--project","X"]}' +``` + +Esto permite que el agente de Antigravity (Jetski) llame tools de hcom: +`send`, `list`, `events`, `transcript`, etc. — directamente desde el chat. + +**Para "tener antigravity como agente trabajador"** (multiples instancias): +No es posible sin una extensión VS Code custom que bridgee hcom ↔ Antigravity. +El `chat --mode agent` siempre abre una ventana Electron — no hay modo headless. + +## Auto-install: `hcom hooks add antigravity` + +La extensión está integrada en el sistema de hooks de hcom: + +```bash +# Instalar la extensión en Antigravity +hcom hooks add antigravity + +# Verificar estado +hcom hooks status | grep antigravity + +# Remover +hcom hooks remove antigravity +``` + +Esto: +1. Escribe `extension.js` + `package.json` en `~/.antigravity/extensions/hcom.hcom-bridge-0.1.0/` +2. Actualiza `~/.antigravity/extensions/extensions.json` +3. La extensión se activa al reiniciar Antigravity + +La extensión compilada (~13KB) está embedida en el binario de hcom via +`include_str!` — no requiere npm/TypeScript en la máquina del usuario. + +### Source + +- Extensión TS: `extensions/hcom-bridge/` +- Build: `extensions/hcom-bridge/build.sh` +- Embed Rust: `src/hooks/antigravity.rs` +- Hook wiring: `src/commands/hooks.rs` + +--- + +*Research by: @sora, @moto, @yuri — May 2026* diff --git a/extensions/hcom-bridge/.gitignore b/extensions/hcom-bridge/.gitignore new file mode 100644 index 00000000..b9470778 --- /dev/null +++ b/extensions/hcom-bridge/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/extensions/hcom-bridge/.vscodeignore b/extensions/hcom-bridge/.vscodeignore new file mode 100644 index 00000000..9df5e04d --- /dev/null +++ b/extensions/hcom-bridge/.vscodeignore @@ -0,0 +1,6 @@ +.vscodeignore +node_modules/ +src/ +tsconfig.json +build.sh +.gitignore diff --git a/extensions/hcom-bridge/build.sh b/extensions/hcom-bridge/build.sh new file mode 100644 index 00000000..673d2855 --- /dev/null +++ b/extensions/hcom-bridge/build.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# Build the hcom-bridge VS Code extension for Antigravity +set -e + +cd "$(dirname "$0")" + +echo "Installing dependencies..." +npm install --silent + +echo "Compiling TypeScript..." +npx tsc -p tsconfig.json + +echo "Bundling extension..." +npx esbuild src/extension.ts --bundle --outfile=dist/bundle.js \ + --external:vscode --platform=node --target=node20 --minify + +echo "Copying to embed location..." +cp dist/bundle.js ../../src/antigravity_extension/extension.js +cp package.json ../../src/antigravity_extension/ + +echo "Done. Extension built and embedded at src/antigravity_extension/" +echo "Size: $(wc -c < ../../src/antigravity_extension/extension.js) bytes" diff --git a/extensions/hcom-bridge/package.json b/extensions/hcom-bridge/package.json new file mode 100644 index 00000000..786f70fa --- /dev/null +++ b/extensions/hcom-bridge/package.json @@ -0,0 +1,70 @@ +{ + "name": "hcom-bridge", + "displayName": "hcom Bridge", + "description": "Bidirectional bridge between hcom agents and Antigravity's Jetski agent", + "version": "0.1.0", + "publisher": "hcom", + "license": "MIT", + "engines": { + "vscode": "^1.68.0" + }, + "categories": [ + "Chat", + "Other" + ], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./extension.js", + "contributes": { + "chatParticipants": [ + { + "id": "hcom.hcom", + "name": "hcom", + "fullName": "hcom Agents", + "description": "Send messages to hcom agents and list active agents", + "isSticky": true + } + ], + "commands": [ + { + "command": "hcom.listAgents", + "title": "hcom: List active agents" + }, + { + "command": "hcom.sendMessage", + "title": "hcom: Send message to agent" + }, + { + "command": "hcom.showStatus", + "title": "hcom: Show connection status" + } + ], + "configuration": { + "title": "hcom Bridge", + "properties": { + "hcom.bridge.binaryPath": { + "type": "string", + "default": "hcom", + "description": "Path to the hcom binary. Defaults to 'hcom' in PATH." + }, + "hcom.bridge.project": { + "type": "string", + "default": "", + "description": "hcom project to scope messages to." + }, + "hcom.bridge.autoSendToJetski": { + "type": "boolean", + "default": true, + "description": "Automatically forward incoming hcom messages to the Jetski agent panel." + } + } + } + }, + "dependencies": { + "@types/node": "^25.6.0", + "@types/vscode": "^1.118.0", + "esbuild": "^0.28.0", + "typescript": "^6.0.3" + } +} diff --git a/extensions/hcom-bridge/src/antigravity.d.ts b/extensions/hcom-bridge/src/antigravity.d.ts new file mode 100644 index 00000000..b373bc88 --- /dev/null +++ b/extensions/hcom-bridge/src/antigravity.d.ts @@ -0,0 +1,10 @@ +declare namespace antigravityExtensibility { + function sendToAgentPanel(options: { + message?: string; + files?: { uri: import('vscode').Uri; lineRange?: [number, number] }[]; + autoSend?: boolean; + }): void; + function refreshMcpServers(): void; + function writeMcpConfig(pluginName: string, content: string): void; + function getMcpConfig(pluginName: string): string | undefined; +} diff --git a/extensions/hcom-bridge/src/extension.ts b/extensions/hcom-bridge/src/extension.ts new file mode 100644 index 00000000..c6bb4f14 --- /dev/null +++ b/extensions/hcom-bridge/src/extension.ts @@ -0,0 +1,67 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import { HcomClient } from './hcomClient'; +import { JetskiBridge } from './jetskiBridge'; + +let hcomClient: HcomClient | undefined; +let jetskiBridge: JetskiBridge | undefined; + +function logToFile(level: string, msg: string): void { + try { + const home = process.env.HOME || process.env.USERPROFILE || ''; + const logDir = path.join(home, '.hcom', 'extensions'); + fs.mkdirSync(logDir, { recursive: true }); + const ts = new Date().toISOString(); + fs.appendFileSync(path.join(logDir, 'hcom-bridge.log'), `[${ts}] [${level}] [ext] ${msg}\n`); + } catch { } +} + +function deriveAgentName(): string | null { + const folder = vscode.workspace.workspaceFolders?.[0]; + if (!folder) return null; + const base = folder.name.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); + return base ? `antigravity-${base}` : null; +} + +function deriveProjectName(): string { + const folder = vscode.workspace.workspaceFolders?.[0]; + if (!folder) return ''; + return folder.name.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); +} + +export async function activate(context: vscode.ExtensionContext): Promise { + const name = deriveAgentName(); + hcomClient = new HcomClient(); + jetskiBridge = new JetskiBridge(hcomClient, name); + context.subscriptions.push(hcomClient); + context.subscriptions.push(jetskiBridge); + + if (name) { + jetskiBridge.updateStatusBar('registering'); + const project = deriveProjectName(); + await hcomClient.startAgent(name, project); + } + + hcomClient.start(); + jetskiBridge.activate(context); + + if (name) { + jetskiBridge.updateStatusBar(`registered as ${name}`); + } else { + jetskiBridge.updateStatusBar('no agent (no workspace)'); + } + logToFile('INFO', `activated${name ? ` as agent "${name}"` : ''}`); +} + +export async function deactivate(): Promise { + if (hcomClient) { + await hcomClient.stopAgent(); + hcomClient.dispose(); + hcomClient = undefined; + } + if (jetskiBridge) { + jetskiBridge.dispose(); + jetskiBridge = undefined; + } + logToFile('INFO', 'deactivated'); +} diff --git a/extensions/hcom-bridge/src/hcomClient.ts b/extensions/hcom-bridge/src/hcomClient.ts new file mode 100644 index 00000000..c3a237f7 --- /dev/null +++ b/extensions/hcom-bridge/src/hcomClient.ts @@ -0,0 +1,301 @@ +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; + +type MessageHandler = (sender: string, text: string) => void; + +/** Write a log line to ~/.hcom/extensions/hcom-bridge.log */ +function logToFile(level: string, msg: string): void { + try { + const home = process.env.HOME || process.env.USERPROFILE || ''; + const logDir = path.join(home, '.hcom', 'extensions'); + fs.mkdirSync(logDir, { recursive: true }); + const ts = new Date().toISOString(); + fs.appendFileSync(path.join(logDir, 'hcom-bridge.log'), `[${ts}] [${level}] ${msg}\n`); + } catch { /* ignore file errors */ } +} + +/** Common locations to look for the hcom binary. */ +const COMMON_HCOM_PATHS = [ + 'hcom', + path.join(process.env.HOME || '', '.local', 'bin', 'hcom'), + path.join(process.env.HOME || '', '.cargo', 'bin', 'hcom'), + '/usr/local/bin/hcom', + '/opt/homebrew/bin/hcom', +]; + +function resolveBinaryPath(): string { + const config = vscode.workspace.getConfiguration('hcom.bridge'); + const configured = config.get('binaryPath', 'hcom'); + if (configured && configured !== 'hcom') { + logToFile('INFO', `using configured binary path: ${configured}`); + return configured; + } + for (const p of COMMON_HCOM_PATHS) { + if (p === 'hcom') continue; + if (fs.existsSync(p)) { + logToFile('INFO', `resolved hcom binary: ${p}`); + return p; + } + } + logToFile('WARN', 'hcom binary not found at common paths, using "hcom" (PATH)'); + return 'hcom'; +} + +export class HcomClient implements vscode.Disposable { + private process: cp.ChildProcess | null = null; + private onMessageHandlers: MessageHandler[] = []; + private onStatusHandlers: Array<(status: string) => void> = []; + private restartTimeout: ReturnType | null = null; + private _disposed = false; + private _agentName: string | null = null; + + get isRunning(): boolean { + return this.process !== null && !this.process.killed; + } + + get agentName(): string | null { + return this._agentName; + } + + onMessage(handler: MessageHandler): vscode.Disposable { + this.onMessageHandlers.push(handler); + return { dispose: () => this.offMessage(handler) }; + } + + onStatus(handler: (status: string) => void): vscode.Disposable { + this.onStatusHandlers.push(handler); + return { dispose: () => this.offStatus(handler) }; + } + + private offMessage(handler: MessageHandler): void { + this.onMessageHandlers = this.onMessageHandlers.filter(h => h !== handler); + } + + private offStatus(handler: (status: string) => void): void { + this.onStatusHandlers = this.onStatusHandlers.filter(h => h !== handler); + } + + private emitMessage(sender: string, text: string): void { + for (const handler of this.onMessageHandlers) { + try { handler(sender, text); } catch { } + } + } + + private emitStatus(status: string): void { + for (const handler of this.onStatusHandlers) { + try { handler(status); } catch { } + } + } + + start(): void { + if (this._disposed) return; + this.spawn(); + } + + /** Register as an hcom agent so other agents can send messages to us. */ + async startAgent(name: string, project?: string): Promise { + this._agentName = name; + const binaryPath = resolveBinaryPath(); + this.emitStatus(`registering as ${name}`); + try { + await new Promise((resolve, reject) => { + cp.execFile(binaryPath, ['start', '--name', name], { timeout: 10000 }, (err, stdout) => { + if (err) reject(new Error(err.message)); + else { + console.log(`hcom agent registered: ${name}`); + this.emitStatus(`registered as ${name}`); + resolve(); + } + }); + }); + // Set project isolation so agent appears in the right project + if (project) { + try { + await new Promise((resolve, reject) => { + cp.execFile(binaryPath, ['config', '-i', name, 'project', project], { timeout: 5000 }, (err) => { + if (err) console.error('set project:', err.message); + else console.log(`project set to ${project} for ${name}`); + resolve(); + }); + }); + } catch { } + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error('hcom start failed:', msg); + this.emitStatus(`register error: ${msg}`); + } + } + + /** Unregister the hcom agent. */ + async stopAgent(): Promise { + const name = this._agentName; + if (!name) return; + this._agentName = null; + this.emitStatus('unregistering'); + const binaryPath = resolveBinaryPath(); + try { + await new Promise((resolve, reject) => { + cp.execFile(binaryPath, ['stop', name], { timeout: 10000 }, (err) => { + if (err) console.error('hcom stop:', err.message); + else console.log(`hcom agent unregistered: ${name}`); + resolve(); + }); + }); + } catch { } + } + + private spawn(): void { + if (this._disposed) return; + this.kill(); + + const binaryPath = resolveBinaryPath(); + const project = vscode.workspace.getConfiguration('hcom.bridge').get('project', ''); + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + + const args = ['listen', '--json']; + if (this._agentName) { + args.push('--name', this._agentName); + } else if (project) { + args.push('--project', project); + } + + const options: cp.SpawnOptions = { + stdio: ['ignore', 'pipe', 'pipe'], + detached: false, + }; + + logToFile('INFO', `spawning: ${binaryPath} ${args.join(' ')}`); + this.emitStatus('connecting'); + const proc = cp.spawn(binaryPath, args, options); + this.process = proc; + + let buffer = ''; + proc.stdout?.on('data', (data: Buffer) => { + const text = data.toString(); + logToFile('DEBUG', `stdout: ${text.trim().slice(0, 200)}`); + buffer += text; + this.parseBuffer(buffer, msg => { + buffer = buffer.slice(buffer.indexOf(msg) + msg.length + 1); + const parsed = this.parseHcomMessage(msg); + if (parsed) { + this.emitMessage(parsed.sender, parsed.text); + } + }); + }); + + proc.stderr?.on('data', (data: Buffer) => { + const text = data.toString().trim(); + if (text) { + logToFile('INFO', `stderr: ${text.slice(0, 200)}`); + this.emitStatus(`stderr: ${text}`); + } + }); + + proc.on('error', (err) => { + logToFile('ERROR', `spawn error: ${err.message}`); + this.emitStatus(`error: ${err.message}`); + this.scheduleRestart(); + }); + + proc.on('exit', (code) => { + logToFile('INFO', `process exited (code=${code})`); + this.process = null; + this.emitStatus(`exited (${code})`); + if (!this._disposed) { + this.scheduleRestart(); + } + }); + + this.emitStatus('listening'); + logToFile('INFO', `listen spawned successfully, pid=${proc.pid}`); + } + + private parseBuffer(buffer: string, callback: (msg: string) => void): void { + const lines = buffer.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith('{') && trimmed.endsWith('}')) { + callback(trimmed); + } + } + } + + private parseHcomMessage(line: string): { sender: string; text: string } | null { + // JSON mode: {"from":"hiro","text":"5+9?"} + try { + const obj = JSON.parse(line); + const from = obj.from; + const text = obj.text; + if (from && text) { + return { sender: from, text }; + } + } catch { } + return null; + } + + async send(target: string, message: string): Promise { + const binaryPath = resolveBinaryPath(); + const project = vscode.workspace.getConfiguration('hcom.bridge').get('project', ''); + + const args = ['send', `@${target}`, '--', message]; + if (project) { + args.push('--project', project); + } + + return new Promise((resolve, reject) => { + cp.execFile(binaryPath, args, { timeout: 10000 }, (err, stdout, stderr) => { + if (err) reject(new Error(stderr || err.message)); + else resolve(stdout.trim()); + }); + }); + } + + async listAgents(): Promise { + const binaryPath = resolveBinaryPath(); + const project = vscode.workspace.getConfiguration('hcom.bridge').get('project', ''); + + const args = ['list', '--names']; + if (project) { + args.push('--project', project); + } + + return new Promise((resolve, reject) => { + cp.execFile(binaryPath, args, { timeout: 5000 }, (err, stdout) => { + if (err) reject(err); + else resolve(stdout.trim()); + }); + }); + } + + private scheduleRestart(): void { + if (this._disposed) return; + if (this.restartTimeout) { + clearTimeout(this.restartTimeout); + } + this.restartTimeout = setTimeout(() => this.spawn(), 3000); + this.emitStatus('reconnecting in 3s...'); + } + + private kill(): void { + if (this.restartTimeout) { + clearTimeout(this.restartTimeout); + this.restartTimeout = null; + } + if (this.process && !this.process.killed) { + try { + this.process.kill(); + } catch { } + } + this.process = null; + } + + dispose(): void { + this._disposed = true; + this.kill(); + this.onMessageHandlers = []; + this.onStatusHandlers = []; + } +} diff --git a/extensions/hcom-bridge/src/jetskiBridge.ts b/extensions/hcom-bridge/src/jetskiBridge.ts new file mode 100644 index 00000000..7c4afdc5 --- /dev/null +++ b/extensions/hcom-bridge/src/jetskiBridge.ts @@ -0,0 +1,245 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import { HcomClient } from './hcomClient'; + +/// + +/** Write a log line to ~/.hcom/extensions/hcom-bridge.log */ +function logToFile(level: string, msg: string): void { + try { + const home = process.env.HOME || process.env.USERPROFILE || ''; + const logDir = path.join(home, '.hcom', 'extensions'); + fs.mkdirSync(logDir, { recursive: true }); + const ts = new Date().toISOString(); + fs.appendFileSync(path.join(logDir, 'hcom-bridge.log'), `[${ts}] [${level}] [jetski] ${msg}\n`); + } catch { /* ignore */ } +} + +export class JetskiBridge implements vscode.Disposable { + private hcomClient: HcomClient; + private statusBarItem: vscode.StatusBarItem; + private disposables: vscode.Disposable[] = []; + private _agentName: string | null = null; + private _conversationStarted = false; + private _msgCount = 0; + + constructor(hcomClient: HcomClient, agentName: string | null, private _projectName: string = '') { + this.hcomClient = hcomClient; + this._agentName = agentName; + this.statusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, 100 + ); + this.statusBarItem.command = 'hcom.showStatus'; + this.statusBarItem.tooltip = 'hcom Bridge — click for status'; + this.statusBarItem.show(); + this.updateStatusBar('initializing'); + } + + activate(context: vscode.ExtensionContext): void { + this.registerListeners(); + this.registerCommands(context); + this.registerChatParticipant(context); + this.updateStatusBar('active'); + } + + private registerListeners(): void { + this.disposables.push( + this.hcomClient.onMessage((sender, text) => { + this.forwardToJetski(sender, text); + }) + ); + + this.disposables.push( + this.hcomClient.onStatus((status) => { + this.updateStatusBar(status); + }) + ); + } + + private registerCommands(context: vscode.ExtensionContext): void { + context.subscriptions.push( + vscode.commands.registerCommand('hcom.listAgents', async () => { + try { + const agents = await this.hcomClient.listAgents(); + vscode.window.showInformationMessage(`hcom agents:\n${agents}`); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage(`Failed to list agents: ${msg}`); + } + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand('hcom.sendMessage', async () => { + const target = await vscode.window.showInputBox({ + prompt: 'Target agent name', + placeHolder: 'e.g. moto, sora, yuri' + }); + if (!target) return; + + const message = await vscode.window.showInputBox({ + prompt: `Message to @${target}`, + placeHolder: 'Enter your message' + }); + if (!message) return; + + try { + const result = await this.hcomClient.send(target, message); + vscode.window.showInformationMessage(`Sent: ${result}`); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage(`Send failed: ${msg}`); + } + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand('hcom.showStatus', () => { + const running = this.hcomClient.isRunning; + const status = running ? 'Connected' : 'Disconnected'; + const httpPort = vscode.workspace.getConfiguration('antigravityBridge').get('httpPort', 5000); + const hasAutomation = running; // we'll detect this at runtime + vscode.window.showInformationMessage( + `hcom Bridge: ${status}\n\n` + + `Agent: ${this._agentName || '(none)'}\n` + + `Binary: ${vscode.workspace.getConfiguration('hcom.bridge').get('binaryPath', 'hcom')}\n` + + `Project: ${vscode.workspace.getConfiguration('hcom.bridge').get('project', '(none)')}\n` + + `Automation API: http://localhost:${httpPort}` + ); + }) + ); + } + + private registerChatParticipant(context: vscode.ExtensionContext): void { + const participant = vscode.chat.createChatParticipant('hcom.hcom', async ( + request: vscode.ChatRequest, + _context: vscode.ChatContext, + response: vscode.ChatResponseStream, + _token: vscode.CancellationToken + ) => { + const prompt = request.prompt.trim(); + + if (prompt.startsWith('list') || prompt === 'agents' || prompt === 'ls') { + try { + const agents = await this.hcomClient.listAgents(); + response.markdown(`**hcom active agents:**\n\n\`\`\`\n${agents}\n\`\`\``); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + response.markdown(`Error: ${msg}`); + } + return; + } + + const sendMatch = prompt.match(/^(?:send\s+)?@?(\S+)\s+(.+)/); + if (sendMatch) { + const target = sendMatch[1]; + const message = sendMatch[2]; + try { + await this.hcomClient.send(target, message); + response.markdown(`✅ Sent message to @${target}`); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + response.markdown(`❌ Failed: ${msg}`); + } + return; + } + + response.markdown( + `Usage:\n\n` + + `- \`@hcom list\` — list active agents\n` + + `- \`@hcom send @name message\` — send message to agent\n` + + `- \`@hcom @name message\` — shorthand (auto-detect as send)\n` + + `- \`@hcom status\` — show bridge status\n` + + `- \`@hcom help\` — this help` + ); + }); + + participant.followupProvider = { + provideFollowups(_result, _token) { + return [ + { prompt: 'list', label: 'List agents' }, + { prompt: 'status', label: 'Bridge status' }, + ]; + } + }; + + context.subscriptions.push(participant); + } + + private forwardToJetski(sender: string, text: string): void { + const config = vscode.workspace.getConfiguration('hcom.bridge'); + const autoSend = config.get('autoSendToJetski', true); + if (!autoSend) { logToFile('INFO', `autoSend disabled`); return; } + this.sendToAgentPanel(sender, text); + } + + /** Use native Antigravity commands to send messages to the agent panel. */ + private async sendToAgentPanel(sender: string, text: string): Promise { + logToFile('INFO', `sending msg from ${sender} to agent panel`); + try { + if (!this._conversationStarted) { + try { + await vscode.commands.executeCommand('antigravity.startNewConversation'); + this._conversationStarted = true; + logToFile('INFO', 'new conversation started'); + await new Promise(r => setTimeout(r, 2000)); + } catch (e) { + logToFile('WARN', `startNewConversation failed: ${e}`); + } + } + + this._msgCount++; + const withInstructions = this._msgCount === 1 || this._msgCount % 15 === 0; + + const agentName = this._agentName || 'antigravity'; + const instructions = withInstructions + ? `You are connected to hcom as "${agentName}".\n\ +Respond using terminal: hcom send @${sender} --name ${agentName} -- "your response"\n\n` : ''; + + await vscode.commands.executeCommand( + 'antigravity.sendPromptToAgentPanel', + `${instructions}**[hcom message from @${sender}]**\n\n${text}` + ); + logToFile('INFO', `msg #${this._msgCount} sent${withInstructions ? ' + instructions' : ''}`); + this.updateStatusBar(`sent to antigravity agent`); + } catch (e) { + logToFile('ERROR', `send to agent panel failed: ${e}`); + this.updateStatusBar('send failed'); + } + } + + /** Update the status bar display. Public so extension.ts can show registration state. */ + updateStatusBar(status: string): void { + const icons: Record = { + 'active': '$(broadcast)', + 'listening': '$(broadcast)', + 'connected': '$(broadcast)', + 'registered': '$(broadcast)', + 'connecting': '$(sync~spin)', + 'reconnecting': '$(sync~spin)', + 'reconnecting in 3s': '$(sync~spin)', + 'registering': '$(sync~spin)', + 'error': '$(error)', + 'no antigravity API': '$(warning)', + 'exited': '$(debug-disconnect)', + 'initializing': '$(loading~spin)', + 'no agent': '$(debug-disconnect)', + }; + const icon = Object.entries(icons).find(([k]) => status.startsWith(k))?.[1] || '$(question)'; + const label = this._agentName ? ` ${this._agentName}` : ''; + this.statusBarItem.text = `${icon} hcom${label}`; + this.statusBarItem.tooltip = `hcom Bridge — ${status}${this._agentName ? `\nAgent: ${this._agentName}` : ''}`; + this.statusBarItem.backgroundColor = status.includes('error') || status.startsWith('exited') + ? new vscode.ThemeColor('statusBarItem.errorBackground') + : undefined; + } + + dispose(): void { + this.statusBarItem.dispose(); + for (const d of this.disposables) { + d.dispose(); + } + this.disposables = []; + } +} diff --git a/extensions/hcom-bridge/tsconfig.json b/extensions/hcom-bridge/tsconfig.json new file mode 100644 index 00000000..d61767b2 --- /dev/null +++ b/extensions/hcom-bridge/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "types": ["node"], + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/src/agent_prompts.rs b/src/agent_prompts.rs new file mode 100644 index 00000000..6f1f0950 --- /dev/null +++ b/src/agent_prompts.rs @@ -0,0 +1,47 @@ +use std::path::PathBuf; + +const AGENTS_DIR: &str = "agents"; + +fn get_agents_dir() -> PathBuf { + let hcom_dir = crate::config::Config::get().hcom_dir; + hcom_dir.join(AGENTS_DIR) +} + +pub fn get_agent_prompt_path(instance_name: &str) -> PathBuf { + get_agents_dir().join(format!("{}.md", instance_name)) +} + +pub fn load_agent_prompt(instance_name: &str) -> Option { + let path = get_agent_prompt_path(instance_name); + if path.exists() { + std::fs::read_to_string(&path).ok().filter(|s| !s.trim().is_empty()) + } else { + None + } +} + +pub fn ensure_agents_dir() -> std::io::Result<()> { + std::fs::create_dir_all(get_agents_dir()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_load_nonexistent_returns_none() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("nonexistent.md"); + assert!(!path.exists()); + } + + #[test] + fn test_ensure_agents_dir_creates() { + let tmp = TempDir::new().unwrap(); + let dir = tmp.path().join("agents"); + assert!(!dir.exists()); + std::fs::create_dir_all(&dir).unwrap(); + assert!(dir.exists()); + } +} diff --git a/src/antigravity_extension/extension.js b/src/antigravity_extension/extension.js new file mode 100644 index 00000000..69751e1c --- /dev/null +++ b/src/antigravity_extension/extension.js @@ -0,0 +1,21 @@ +"use strict";var H=Object.create;var C=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var E=Object.getPrototypeOf,T=Object.prototype.hasOwnProperty;var A=(n,t)=>{for(var e in t)C(n,e,{get:t[e],enumerable:!0})},x=(n,t,e,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of j(t))!T.call(n,s)&&s!==e&&C(n,s,{get:()=>t[s],enumerable:!(o=O(t,s))||o.enumerable});return n};var h=(n,t,e)=>(e=n!=null?H(E(n)):{},x(t||!n||!n.__esModule?C(e,"default",{value:n,enumerable:!0}):e,n)),R=n=>x(C({},"__esModule",{value:!0}),n);var z={};A(z,{activate:()=>J,deactivate:()=>U});module.exports=R(z);var _=h(require("vscode")),I=h(require("fs"));var f=h(require("child_process")),$=h(require("fs")),b=h(require("path")),w=h(require("vscode"));function p(n,t){try{let e=process.env.HOME||process.env.USERPROFILE||"",o=b.join(e,".hcom","extensions");$.mkdirSync(o,{recursive:!0});let s=new Date().toISOString();$.appendFileSync(b.join(o,"hcom-bridge.log"),`[${s}] [${n}] ${t} +`)}catch{}}var D=["hcom",b.join(process.env.HOME||"",".local","bin","hcom"),b.join(process.env.HOME||"",".cargo","bin","hcom"),"/usr/local/bin/hcom","/opt/homebrew/bin/hcom"];function y(){let t=w.workspace.getConfiguration("hcom.bridge").get("binaryPath","hcom");if(t&&t!=="hcom")return p("INFO",`using configured binary path: ${t}`),t;for(let e of D)if(e!=="hcom"&&$.existsSync(e))return p("INFO",`resolved hcom binary: ${e}`),e;return p("WARN",'hcom binary not found at common paths, using "hcom" (PATH)'),"hcom"}var k=class{constructor(){this.process=null;this.onMessageHandlers=[];this.onStatusHandlers=[];this.restartTimeout=null;this._disposed=!1;this._agentName=null}get isRunning(){return this.process!==null&&!this.process.killed}get agentName(){return this._agentName}onMessage(t){return this.onMessageHandlers.push(t),{dispose:()=>this.offMessage(t)}}onStatus(t){return this.onStatusHandlers.push(t),{dispose:()=>this.offStatus(t)}}offMessage(t){this.onMessageHandlers=this.onMessageHandlers.filter(e=>e!==t)}offStatus(t){this.onStatusHandlers=this.onStatusHandlers.filter(e=>e!==t)}emitMessage(t,e){for(let o of this.onMessageHandlers)try{o(t,e)}catch{}}emitStatus(t){for(let e of this.onStatusHandlers)try{e(t)}catch{}}start(){this._disposed||this.spawn()}async startAgent(t,e){this._agentName=t;let o=y();this.emitStatus(`registering as ${t}`);try{if(await new Promise((s,i)=>{f.execFile(o,["start","--name",t],{timeout:1e4},(a,g)=>{a?i(new Error(a.message)):(console.log(`hcom agent registered: ${t}`),this.emitStatus(`registered as ${t}`),s())})}),e)try{await new Promise((s,i)=>{f.execFile(o,["config","-i",t,"project",e],{timeout:5e3},a=>{a?console.error("set project:",a.message):console.log(`project set to ${e} for ${t}`),s()})})}catch{}}catch(s){let i=s instanceof Error?s.message:String(s);console.error("hcom start failed:",i),this.emitStatus(`register error: ${i}`)}}async stopAgent(){let t=this._agentName;if(!t)return;this._agentName=null,this.emitStatus("unregistering");let e=y();try{await new Promise((o,s)=>{f.execFile(e,["stop",t],{timeout:1e4},i=>{i?console.error("hcom stop:",i.message):console.log(`hcom agent unregistered: ${t}`),o()})})}catch{}}spawn(){if(this._disposed)return;this.kill();let t=y(),e=w.workspace.getConfiguration("hcom.bridge").get("project",""),o=w.workspace.workspaceFolders?.[0]?.uri.fsPath,s=["listen","--json"];this._agentName?s.push("--name",this._agentName):e&&s.push("--project",e);let i={stdio:["ignore","pipe","pipe"],detached:!1};p("INFO",`spawning: ${t} ${s.join(" ")}`),this.emitStatus("connecting");let a=f.spawn(t,s,i);this.process=a;let g="";a.stdout?.on("data",d=>{let c=d.toString();p("DEBUG",`stdout: ${c.trim().slice(0,200)}`),g+=c,this.parseBuffer(g,m=>{g=g.slice(g.indexOf(m)+m.length+1);let v=this.parseHcomMessage(m);v&&this.emitMessage(v.sender,v.text)})}),a.stderr?.on("data",d=>{let c=d.toString().trim();c&&(p("INFO",`stderr: ${c.slice(0,200)}`),this.emitStatus(`stderr: ${c}`))}),a.on("error",d=>{p("ERROR",`spawn error: ${d.message}`),this.emitStatus(`error: ${d.message}`),this.scheduleRestart()}),a.on("exit",d=>{p("INFO",`process exited (code=${d})`),this.process=null,this.emitStatus(`exited (${d})`),this._disposed||this.scheduleRestart()}),this.emitStatus("listening"),p("INFO",`listen spawned successfully, pid=${a.pid}`)}parseBuffer(t,e){let o=t.split(` +`);for(let s of o){let i=s.trim();i.startsWith("{")&&i.endsWith("}")&&e(i)}}parseHcomMessage(t){try{let e=JSON.parse(t),o=e.from,s=e.text;if(o&&s)return{sender:o,text:s}}catch{}return null}async send(t,e){let o=y(),s=w.workspace.getConfiguration("hcom.bridge").get("project",""),i=["send",`@${t}`,"--",e];return s&&i.push("--project",s),new Promise((a,g)=>{f.execFile(o,i,{timeout:1e4},(d,c,m)=>{d?g(new Error(m||d.message)):a(c.trim())})})}async listAgents(){let t=y(),e=w.workspace.getConfiguration("hcom.bridge").get("project",""),o=["list","--names"];return e&&o.push("--project",e),new Promise((s,i)=>{f.execFile(t,o,{timeout:5e3},(a,g)=>{a?i(a):s(g.trim())})})}scheduleRestart(){this._disposed||(this.restartTimeout&&clearTimeout(this.restartTimeout),this.restartTimeout=setTimeout(()=>this.spawn(),3e3),this.emitStatus("reconnecting in 3s..."))}kill(){if(this.restartTimeout&&(clearTimeout(this.restartTimeout),this.restartTimeout=null),this.process&&!this.process.killed)try{this.process.kill()}catch{}this.process=null}dispose(){this._disposed=!0,this.kill(),this.onMessageHandlers=[],this.onStatusHandlers=[]}};var r=h(require("vscode")),B=h(require("fs")),N=h(require("path"));function S(n,t){try{let e=process.env.HOME||process.env.USERPROFILE||"",o=N.join(e,".hcom","extensions");B.mkdirSync(o,{recursive:!0});let s=new Date().toISOString();B.appendFileSync(N.join(o,"hcom-bridge.log"),`[${s}] [${n}] [jetski] ${t} +`)}catch{}}var P=class{constructor(t,e,o=""){this._projectName=o;this.disposables=[];this._agentName=null;this._conversationStarted=!1;this._msgCount=0;this.hcomClient=t,this._agentName=e,this.statusBarItem=r.window.createStatusBarItem(r.StatusBarAlignment.Left,100),this.statusBarItem.command="hcom.showStatus",this.statusBarItem.tooltip="hcom Bridge \u2014 click for status",this.statusBarItem.show(),this.updateStatusBar("initializing")}activate(t){this.registerListeners(),this.registerCommands(t),this.registerChatParticipant(t),this.updateStatusBar("active")}registerListeners(){this.disposables.push(this.hcomClient.onMessage((t,e)=>{this.forwardToJetski(t,e)})),this.disposables.push(this.hcomClient.onStatus(t=>{this.updateStatusBar(t)}))}registerCommands(t){t.subscriptions.push(r.commands.registerCommand("hcom.listAgents",async()=>{try{let e=await this.hcomClient.listAgents();r.window.showInformationMessage(`hcom agents: +${e}`)}catch(e){let o=e instanceof Error?e.message:String(e);r.window.showErrorMessage(`Failed to list agents: ${o}`)}})),t.subscriptions.push(r.commands.registerCommand("hcom.sendMessage",async()=>{let e=await r.window.showInputBox({prompt:"Target agent name",placeHolder:"e.g. moto, sora, yuri"});if(!e)return;let o=await r.window.showInputBox({prompt:`Message to @${e}`,placeHolder:"Enter your message"});if(o)try{let s=await this.hcomClient.send(e,o);r.window.showInformationMessage(`Sent: ${s}`)}catch(s){let i=s instanceof Error?s.message:String(s);r.window.showErrorMessage(`Send failed: ${i}`)}})),t.subscriptions.push(r.commands.registerCommand("hcom.showStatus",()=>{let e=this.hcomClient.isRunning,o=e?"Connected":"Disconnected",s=r.workspace.getConfiguration("antigravityBridge").get("httpPort",5e3),i=e;r.window.showInformationMessage(`hcom Bridge: ${o} + +Agent: ${this._agentName||"(none)"} +Binary: ${r.workspace.getConfiguration("hcom.bridge").get("binaryPath","hcom")} +Project: ${r.workspace.getConfiguration("hcom.bridge").get("project","(none)")} +Automation API: http://localhost:${s}`)}))}registerChatParticipant(t){let e=r.chat.createChatParticipant("hcom.hcom",async(o,s,i,a)=>{let g=o.prompt.trim();if(g.startsWith("list")||g==="agents"||g==="ls"){try{let c=await this.hcomClient.listAgents();i.markdown(`**hcom active agents:** + +\`\`\` +${c} +\`\`\``)}catch(c){let m=c instanceof Error?c.message:String(c);i.markdown(`Error: ${m}`)}return}let d=g.match(/^(?:send\s+)?@?(\S+)\s+(.+)/);if(d){let c=d[1],m=d[2];try{await this.hcomClient.send(c,m),i.markdown(`\u2705 Sent message to @${c}`)}catch(v){let F=v instanceof Error?v.message:String(v);i.markdown(`\u274C Failed: ${F}`)}return}i.markdown("Usage:\n\n- `@hcom list` \u2014 list active agents\n- `@hcom send @name message` \u2014 send message to agent\n- `@hcom @name message` \u2014 shorthand (auto-detect as send)\n- `@hcom status` \u2014 show bridge status\n- `@hcom help` \u2014 this help")});e.followupProvider={provideFollowups(o,s){return[{prompt:"list",label:"List agents"},{prompt:"status",label:"Bridge status"}]}},t.subscriptions.push(e)}forwardToJetski(t,e){if(!r.workspace.getConfiguration("hcom.bridge").get("autoSendToJetski",!0)){S("INFO","autoSend disabled");return}this.sendToAgentPanel(t,e)}async sendToAgentPanel(t,e){S("INFO",`sending msg from ${t} to agent panel`);try{if(!this._conversationStarted)try{await r.commands.executeCommand("antigravity.startNewConversation"),this._conversationStarted=!0,S("INFO","new conversation started"),await new Promise(a=>setTimeout(a,2e3))}catch(a){S("WARN",`startNewConversation failed: ${a}`)}this._msgCount++;let o=this._msgCount===1||this._msgCount%15===0,s=this._agentName||"antigravity",i=o?`You are connected to hcom as "${s}". +Respond using terminal: hcom send @${t} --name ${s} -- "your response" + +`:"";await r.commands.executeCommand("antigravity.sendPromptToAgentPanel",`${i}**[hcom message from @${t}]** + +${e}`),S("INFO",`msg #${this._msgCount} sent${o?" + instructions":""}`),this.updateStatusBar("sent to antigravity agent")}catch(o){S("ERROR",`send to agent panel failed: ${o}`),this.updateStatusBar("send failed")}}updateStatusBar(t){let o=Object.entries({active:"$(broadcast)",listening:"$(broadcast)",connected:"$(broadcast)",registered:"$(broadcast)",connecting:"$(sync~spin)",reconnecting:"$(sync~spin)","reconnecting in 3s":"$(sync~spin)",registering:"$(sync~spin)",error:"$(error)","no antigravity API":"$(warning)",exited:"$(debug-disconnect)",initializing:"$(loading~spin)","no agent":"$(debug-disconnect)"}).find(([i])=>t.startsWith(i))?.[1]||"$(question)",s=this._agentName?` ${this._agentName}`:"";this.statusBarItem.text=`${o} hcom${s}`,this.statusBarItem.tooltip=`hcom Bridge \u2014 ${t}${this._agentName?` +Agent: ${this._agentName}`:""}`,this.statusBarItem.backgroundColor=t.includes("error")||t.startsWith("exited")?new r.ThemeColor("statusBarItem.errorBackground"):void 0}dispose(){this.statusBarItem.dispose();for(let t of this.disposables)t.dispose();this.disposables=[]}};var l,u;function M(n,t){try{let e=process.env.HOME||process.env.USERPROFILE||"",o=path.join(e,".hcom","extensions");I.mkdirSync(o,{recursive:!0});let s=new Date().toISOString();I.appendFileSync(path.join(o,"hcom-bridge.log"),`[${s}] [${n}] [ext] ${t} +`)}catch{}}function L(){let n=_.workspace.workspaceFolders?.[0];if(!n)return null;let t=n.name.replace(/[^a-zA-Z0-9_-]/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"");return t?`antigravity-${t}`:null}function W(){let n=_.workspace.workspaceFolders?.[0];return n?n.name.replace(/[^a-zA-Z0-9_-]/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,""):""}async function J(n){let t=L();if(l=new k,u=new P(l,t),n.subscriptions.push(l),n.subscriptions.push(u),t){u.updateStatusBar("registering");let e=W();await l.startAgent(t,e)}l.start(),u.activate(n),t?u.updateStatusBar(`registered as ${t}`):u.updateStatusBar("no agent (no workspace)"),M("INFO",`activated${t?` as agent "${t}"`:""}`)}async function U(){l&&(await l.stopAgent(),l.dispose(),l=void 0),u&&(u.dispose(),u=void 0),M("INFO","deactivated")}0&&(module.exports={activate,deactivate}); diff --git a/src/antigravity_extension/package.json b/src/antigravity_extension/package.json new file mode 100644 index 00000000..786f70fa --- /dev/null +++ b/src/antigravity_extension/package.json @@ -0,0 +1,70 @@ +{ + "name": "hcom-bridge", + "displayName": "hcom Bridge", + "description": "Bidirectional bridge between hcom agents and Antigravity's Jetski agent", + "version": "0.1.0", + "publisher": "hcom", + "license": "MIT", + "engines": { + "vscode": "^1.68.0" + }, + "categories": [ + "Chat", + "Other" + ], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./extension.js", + "contributes": { + "chatParticipants": [ + { + "id": "hcom.hcom", + "name": "hcom", + "fullName": "hcom Agents", + "description": "Send messages to hcom agents and list active agents", + "isSticky": true + } + ], + "commands": [ + { + "command": "hcom.listAgents", + "title": "hcom: List active agents" + }, + { + "command": "hcom.sendMessage", + "title": "hcom: Send message to agent" + }, + { + "command": "hcom.showStatus", + "title": "hcom: Show connection status" + } + ], + "configuration": { + "title": "hcom Bridge", + "properties": { + "hcom.bridge.binaryPath": { + "type": "string", + "default": "hcom", + "description": "Path to the hcom binary. Defaults to 'hcom' in PATH." + }, + "hcom.bridge.project": { + "type": "string", + "default": "", + "description": "hcom project to scope messages to." + }, + "hcom.bridge.autoSendToJetski": { + "type": "boolean", + "default": true, + "description": "Automatically forward incoming hcom messages to the Jetski agent panel." + } + } + } + }, + "dependencies": { + "@types/node": "^25.6.0", + "@types/vscode": "^1.118.0", + "esbuild": "^0.28.0", + "typescript": "^6.0.3" + } +} diff --git a/src/bootstrap.rs b/src/bootstrap.rs index ec61ec31..45e9075d 100644 --- a/src/bootstrap.rs +++ b/src/bootstrap.rs @@ -54,19 +54,22 @@ Routing rules: You MUST use `hcom --name {instance_name}` for all hcom commands: -- Message: send @name(s) [--intent request|inform|ack] [--reply-to ] [--thread ] -- "message text" +- Message: send @name(s) [--project X] [--intent request|inform|ack] [--reply-to ] [--thread ] -- "message text" Or instead of --: --file | --base64 | pipe/heredoc Example: send @luna @nova --intent ack --reply-to 82 -- "ok" -- See who's active: list [-v] [--json] [--names] [--format '{{name}} {{status}}'] +- See who's active: list [-v] [--json] [--names] [--project X] [--format '{{name}} {{status}}'] - Read another's conversation: transcript [name] [N-M] [--last N] [--full] | transcript search "text" [--all] - View events: events [--last N] [--all] [--sql EXPR] [filters] Filters (same flag=OR, different=AND): --agent NAME | --type message|status|life | --status listening|active|blocked | --cmd PATTERN (contains, ^prefix, =exact) | --file PATH (*.py for glob, file.py for contains) Event-based notifications, watch agents, subscribe, react: events sub [filters] | --help +- Per-agent system prompts: create `~/.hcom/agents/.md` with the role prompt for any agent (self or spawned) + Then `hcom list -v` confirms if file loaded. Re-injected on every session compaction. - Handoff context: bundle prepare -- Spawn agents: [num] [--tag labelOrGroup] [--terminal tmux|kitty|wezterm|etc] +- Spawn agents: [num] [--project X] [--tag labelOrGroup] [--agent-name customName] [--terminal tmux|kitty|wezterm|etc] Example: `hcom 1 claude --tag cool` -> automatic msg when ready -> send it task via hcom send + Example: `hcom 1 claude --agent-name mybot` -> named agent, load ~/.hcom/agents/mybot.md as system prompt Resume: hcom r [args] | Fork: hcom f [args] | Kill: hcom kill - background, set prompt, system, forward args: --help + background, set prompt, system, forward args: --help - Run workflows: run