Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 18 additions & 89 deletions install.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,114 +30,43 @@ If you install only one public piece:
- MCP only -> the user gets tools without discipline
- skills only -> the user gets prose rules without tools

## Step 1: Detect Environment
## Step 1: Detect and Configure (Autopilot)

Determine which CLI or IDE you are currently operating within. This dictates where the skills are cloned and which configuration file holds the MCP server definition.
Hyperstack features an **Autonomous Research-Driven Installer** that identifies your environment (Claude Code, Cursor, Windsurf, Roo Code, etc.) and generates a tailored configuration patch.

## Step 2: Install the Skills
### 1. Identify your Environment
The system automatically probes for signatures:
- **Claude Code**: `CLAUDE_PLUGIN_ROOT`
- **Cursor**: `CURSOR_PLUGIN_ROOT`
- **VS Code Derivatives**: `VSCODE_PID`
- **Known Paths**: Probes for `.claude.json`, `.cursor/`, `.codeium/windsurf/`, `.roo/`, and `.gemini/`.

If the directory already exists (upgrade scenario), pull the latest instead of cloning:
### 2. Run the Autopilot
As an agent, you should call the `hyperstack_setup` tool immediately. It will return the detected path and the JSON configuration block required for that specific tool's schema.

| Environment | Fresh Install | Upgrade |
|---|---|---|
| **Claude Code** | `git clone https://github.com/orkait/hyperstack.git ~/.claude/skills/hyperstack` | `git -C ~/.claude/skills/hyperstack pull` |
| **Cursor** | `git clone https://github.com/orkait/hyperstack.git ~/.cursor/skills/hyperstack` | `git -C ~/.cursor/skills/hyperstack pull` |
| **Antigravity** | `git clone https://github.com/orkait/hyperstack.git ~/.gemini/extensions/hyperstack` | `git -C ~/.gemini/extensions/hyperstack pull` |
| **Gemini CLI** | `git clone https://github.com/orkait/hyperstack.git ~/.gemini/extensions/hyperstack` | `git -C ~/.gemini/extensions/hyperstack pull` |
| **Qwen Code** | `git clone https://github.com/orkait/hyperstack.git ~/.qwen/skills/hyperstack` | `git -C ~/.qwen/skills/hyperstack pull` |
| **Copilot CLI** | Use plugin marketplace if available, otherwise clone into the user's configured skills path | Pull in the cloned directory |
| **OpenCode / Codex** | Follow the platform's file-based skill installation path | Pull in the cloned directory |
| **Other** | If your environment supports local file-based skills, clone the repo to the standard skills/extensions directory for your platform. | Pull in the cloned directory |
**If the environment is unknown:**
1. Call `web_search` with a query like `[IDE Name] MCP configuration path`.
2. Provide the result to `hyperstack_setup(researchResult: "...")`.
3. The tool will generate the final patch.

**Qwen Code note:** Qwen Code does not have a skill plugin system or SessionStart hook. Skills are cloned to `~/.qwen/skills/hyperstack/` and available on disk for the agent to reference manually (via `read_file` or the agent's own knowledge of the repo structure). The 21 skills are not auto-loaded at session start.
## Step 2: Install the Skills

To handle both cases automatically, use the one-liner for your platform:
Clone the repository to a stable location on your system.

**macOS / Linux (Shell):**
```bash
EXT_DIR="$HOME/.gemini/extensions/hyperstack" && \
EXT_DIR="$HOME/.hyperstack" && \
([ -d "$EXT_DIR" ] && git -C "$EXT_DIR" pull || git clone https://github.com/orkait/hyperstack.git "$EXT_DIR")
```

**Windows (PowerShell):**
```powershell
$EXT_DIR = Join-Path $HOME ".gemini/extensions/hyperstack"
$EXT_DIR = Join-Path $HOME ".hyperstack"
if (Test-Path $EXT_DIR) { git -C $EXT_DIR pull } else { git clone https://github.com/orkait/hyperstack.git $EXT_DIR }
```

Replace `~/.claude/skills` or the path variables with the correct path for the current environment (see table above).

The repository includes a `hooks/session-start.mjs` entrypoint that injects the
generated Hyperstack bootstrap into every session automatically. This is how the
enforcement and internal harness model reach the agent without manual
invocation. On platforms that support `.claude-plugin/hooks.json`, this happens
automatically. On others, the skills are still readable but the hook will not
fire.

## Step 3: Configure the MCP Server

Check if Docker is installed and running on the user's system.

### Option A: Docker (Preferred)

Hyperstack uses a **persistent container + `docker exec`** pattern. One long-lived container serves every CLI invocation and every session, so container startup cost is paid once - not on every `claude` run.

**Step 1 - Pull the image:**

```bash
docker pull ghcr.io/orkait/hyperstack:main
```

Pre-pulling is required. MCP servers have a short initialization timeout - if Docker pulls the image on first use it will time out and report as failed.

**Step 2 - Start the persistent container (one-time setup):**

Hyperstack enforces a **singleton container policy**. If a `hyperstack-mcp` container exists OR any container is running from the `hyperstack:main` image, it must be removed before starting a fresh one to ensure no stale state persists.

Run the provided cross-platform singleton enforcement script (requires Bun):
```bash
bun scripts/ensure-singleton.ts
```

Or run the hard check manually:

```bash
# 1. Remove ANY container based on the hyperstack image
# macOS/Linux:
docker ps -aq --filter "ancestor=ghcr.io/orkait/hyperstack:main" | xargs -r docker rm -f

# Windows (PowerShell):
docker ps -aq --filter "ancestor=ghcr.io/orkait/hyperstack:main" | ForEach-Object { docker rm -f $_ }

# 2. Run the fresh singleton container
docker run -d --name hyperstack-mcp --restart unless-stopped \
--memory=512m --cpus=1 \
--entrypoint sleep \
ghcr.io/orkait/hyperstack:main infinity
```

The container stays alive in the background with `sleep infinity` as PID 1. Each MCP session `exec`s a fresh `bun` process inside this container. `--restart unless-stopped` auto-starts the container after Docker restarts. `512m/1 cpu` covers several concurrent sessions.

**Why delete the old container?** An existing `hyperstack-mcp` container may be running a stale image version, have leftover state from a prior install, or use incorrect resource limits. `docker rm -f` ensures every install starts from a known-good baseline. The `2>/dev/null` suppresses the "no such container" error on first-time installs.

Verify it's running:

```bash
docker ps --filter name=hyperstack-mcp
```

**Step 3 - Configure the MCP client:**

Add the following configuration to the appropriate MCP config file for the current environment:

| Environment | Config File |
|---|---|
| **Claude Code** | `~/.claude.json` |
| **Antigravity** | `~/.config/Antigravity/User/mcp.json` |
| **Gemini CLI** | `~/.gemini/config.json` |
| **Qwen Code** | `~/.qwen/settings.json` (global) or `.qwen/settings.json` (project-level) |
| **Cursor / Windsurf / Others** | IDE-specific MCP settings panel or `.mcp.json` in project root |

```json
{
"mcpServers": {
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { designTokensPlugin } from "./plugins/design-tokens/index.js";
import { uiUxPlugin } from "./plugins/ui-ux/index.js";
import { designerPlugin } from "./plugins/designer/index.js";
import { shadcnPlugin } from "./plugins/shadcn/index.js";
import { hyperstackPlugin } from "./plugins/hyperstack/index.js";

const server = new McpServer({
name: "hyperstack",
Expand All @@ -32,6 +33,7 @@ export const allPlugins = [
uiUxPlugin,
designerPlugin,
shadcnPlugin,
hyperstackPlugin,
];

loadPlugins(server, allPlugins);
Expand Down
109 changes: 109 additions & 0 deletions src/internal/setup-hyperstack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";

export interface SetupResult {
detectedPlatform: string;
configPath: string | null;
status: "success" | "pending_research" | "error";
proposedPatch?: any;
message: string;
}

const KNOWN_PLATFORMS: Record<string, { env?: string[]; configFiles: string[] }> = {
"claude-code": {
env: ["CLAUDE_PLUGIN_ROOT"],
configFiles: [".claude.json"],
},
"cursor": {
env: ["CURSOR_PLUGIN_ROOT"],
configFiles: [".cursor/mcp.json"],
},
"windsurf": {
configFiles: [".codeium/windsurf/mcp_config.json"],
},
"roo-code": {
configFiles: [".roo/mcp.json", "mcp_settings.json"],
},
"gemini-cli": {
configFiles: [".gemini/settings.json"],
},
};

export function detectEnvironment(): string {
// Check environment variables first
if (process.env.CLAUDE_PLUGIN_ROOT) return "claude-code";
if (process.env.CURSOR_PLUGIN_ROOT) return "cursor";
if (process.env.VSCODE_PID) return "vscode-derivative"; // Likely Roo Code or Windsurf or VS Code

return "unknown";
}

export function findConfigFile(platform: string): string | null {
const home = os.homedir();

if (platform === "unknown") {
// Probe common locations
for (const [p, info] of Object.entries(KNOWN_PLATFORMS)) {
for (const file of info.configFiles) {
const fullPath = path.join(home, file);
if (fs.existsSync(fullPath)) return fullPath;
}
}
return null;
}

const info = KNOWN_PLATFORMS[platform];
if (!info) return null;

for (const file of info.configFiles) {
const fullPath = path.join(home, file);
if (fs.existsSync(fullPath)) return fullPath;
}

return null;
}

export function generateMcpPatch(configPath: string, pluginRoot: string) {
const binaryPath = path.join(pluginRoot, "bin", "hyperstack.mjs");
const hookPath = path.join(pluginRoot, "hooks", "session-start.mjs");

const serverConfig = {
command: "node",
args: [binaryPath],
env: {
HYPERSTACK_ROOT: pluginRoot
}
};

// Determine schema based on filename
const isClaude = configPath.endsWith(".claude.json");
const isWindsurf = configPath.includes("windsurf");
const isGemini = configPath.includes("gemini");

if (isClaude) {
return {
mcpServers: {
hyperstack: serverConfig
}
};
}

if (isGemini) {
return {
extensions: {
hyperstack: {
...serverConfig,
type: "stdio"
}
}
};
}

// Default MCP schema
return {
mcpServers: {
hyperstack: serverConfig
}
};
}
10 changes: 10 additions & 0 deletions src/plugins/hyperstack/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Plugin } from "../../registry.js";
import { registerSetupTool } from "./tools/setup.js";

export const hyperstackPlugin: Plugin = {
name: "hyperstack",
register: (server: McpServer) => {
registerSetupTool(server);
},
};
50 changes: 50 additions & 0 deletions src/plugins/hyperstack/tools/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import * as setup from "../../../internal/setup-hyperstack.js";
import * as path from "node:path";

export function registerSetupTool(server: McpServer) {
server.tool(
"hyperstack_setup",
"Identify current IDE/CLI environment and generate a tailored MCP configuration patch for Hyperstack.",
{
researchResult: z.string().optional().describe("If the environment was unknown, provide the researched config path or schema details here."),
},
async ({ researchResult }) => {
const platform = setup.detectEnvironment();
const configPath = setup.findConfigFile(platform);

const pluginRoot = process.env.HYPERSTACK_ROOT || process.cwd();

if (!configPath && !researchResult) {
return {
content: [{
type: "text",
text: `Detection failed for environment: ${platform}.\n\nPlease use 'web_search' to find where ${platform} stores its MCP configuration (e.g. 'Cursor MCP config path' or 'Windsurf MCP config location').\n\nOnce found, provide the path as 'researchResult'.`
}]
};
}

const activeConfigPath = configPath || (researchResult ? path.resolve(researchResult) : null);

if (!activeConfigPath) {
return {
isError: true,
content: [{
type: "text",
text: "Could not resolve a valid configuration path."
}]
};
}

const patch = setup.generateMcpPatch(activeConfigPath, pluginRoot);

return {
content: [{
type: "text",
text: `Environment Identified: ${platform}\nTarget Config: ${activeConfigPath}\n\nProposed Patch (JSON):\n${JSON.stringify(patch, null, 2)}\n\nTo apply this, the agent should read the file, merge the 'hyperstack' server entry, and write it back.`
}]
};
}
);
}
20 changes: 20 additions & 0 deletions verify-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as setup from "./src/internal/setup-hyperstack.js";

async function verify() {
console.log("--- Hyperstack Setup Verification ---");
const platform = setup.detectEnvironment();
console.log(`Detected Platform: ${platform}`);

const configPath = setup.findConfigFile(platform);
console.log(`Config Path Found: ${configPath || "None (expected if running in clean environment)"}`);

if (configPath || platform !== "unknown") {
const patch = setup.generateMcpPatch(configPath || "/tmp/mcp.json", process.cwd());
console.log("Proposed Patch:");
console.log(JSON.stringify(patch, null, 2));
} else {
console.log("Environment unknown - research fallback would trigger here.");
}
}

verify().catch(console.error);
Loading