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
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
run: bun run compile:context

- name: Regenerate skills index
run: bash scripts/generate-skills-index.sh
run: bun run skills:index

- name: Run tests
run: bun test
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"test": "bun test",
"start": "bun src/index.ts",
"dev": "bun --watch src/index.ts",
"docker:run": "bun scripts/ensure-singleton.ts"
"docker:run": "bun scripts/ensure-singleton.ts",
"skills:index": "bun scripts/generate-skills-index.ts",
"mcp:start": "bun scripts/start-mcp.ts"
},
"author": "Orkait",
"license": "MIT",
Expand Down
84 changes: 0 additions & 84 deletions scripts/generate-skills-index.sh

This file was deleted.

97 changes: 97 additions & 0 deletions scripts/generate-skills-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, "..");
const SKILLS_DIR = path.join(REPO_ROOT, "skills");
const INDEX_FILE = path.join(SKILLS_DIR, "INDEX.md");

interface SkillInfo {
name: string;
category: string;
description: string;
}

function generateIndex() {
console.log("Generating skills index (cross-platform)...");

const skills: SkillInfo[] = [];

const dirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
.filter(dirent => dirent.isDirectory());

for (const dir of dirs) {
const skillPath = path.join(SKILLS_DIR, dir.name, "SKILL.md");
if (!fs.existsSync(skillPath)) continue;

const content = fs.readFileSync(skillPath, "utf8");

// Simple frontmatter parser
const categoryMatch = content.match(/^category:\s*(.*)/m);
const category = categoryMatch ? categoryMatch[1].trim() : "uncategorized";

// Extract description (handles multi-line and single-line)
const descMatch = content.match(/^description:\s*(?:>|-)?\s*(.*)/m);
let description = descMatch ? descMatch[1].trim() : "";

// If it's multi-line, we might need a more robust parser,
// but for the index, the first line is usually enough.
description = description.replace(/^["'>\-\s]+/, "").substring(0, 120);

skills.push({ name: dir.name, category, description });
}

const core = skills.filter(s => s.category === "core");
const domain = skills.filter(s => s.category === "domain");
const meta = skills.filter(s => s.category === "meta");
const uncategorized = skills.filter(s => !["core", "domain", "meta"].includes(s.category));

const formatTable = (list: SkillInfo[]) => {
if (list.length === 0) return "";
return list.map(s => `| \`${s.name}\` | ${s.description} |`).sort().join("\n") + "\n";
};

const output = `# Hyperstack Skills Index

Auto-generated from each skill's frontmatter \`category\` field.
Regenerate with: \`bun scripts/generate-skills-index.ts\` or \`npm run skills:index\`

Categories:
- **core** - workflow, discipline, and gates used on every task
- **domain** - specialized skills for specific contexts (visual, components, security, docs)
- **meta** - skills about skills (bootstrap, testing)

---

## Core (workflow + discipline)

| Skill | Description |
|---|---|
${formatTable(core)}
## Domain (specialized context)

| Skill | Description |
|---|---|
${formatTable(domain)}
## Meta (skills about skills)

| Skill | Description |
|---|---|
${formatTable(meta)}
${uncategorized.length > 0 ? `
## Uncategorized (missing \`category:\` field)

| Skill | Description |
|---|---|
${formatTable(uncategorized)}

These skills need a \`category:\` added to their frontmatter.
` : ""}
`;

fs.writeFileSync(INDEX_FILE, output);
console.log(`Wrote ${INDEX_FILE}`);
}

generateIndex();
31 changes: 0 additions & 31 deletions scripts/start-mcp.sh

This file was deleted.

74 changes: 74 additions & 0 deletions scripts/start-mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { execSync, spawn } from "child_process";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";

const IMAGE = "ghcr.io/orkait/hyperstack:main";
const CONTAINER_NAME = "hyperstack-mcp";
const LOCK_FILE = path.join(os.tmpdir(), "hyperstack-mcp-startup.lock");

/**
* Standardized Cross-Platform MCP Startup Logic
* - Ensures the persistent container is running.
* - Bridges the local I/O to the containerized MCP process via 'docker exec'.
*/
async function startMcp() {
let lockFd: number | null = null;

try {
// 1. Concurrency control via file lock
try {
lockFd = fs.openSync(LOCK_FILE, 'wx');

// Check if the container is already running
const running = execSync(`docker ps -q --filter "name=^${CONTAINER_NAME}$"`, { stdio: "pipe" }).toString().trim();

if (!running) {
// If not running, ensure it's removed and start fresh
// Suppress errors if container doesn't exist
try { execSync(`docker rm -f ${CONTAINER_NAME}`, { stdio: "ignore" }); } catch {}

execSync(
`docker run -d --name ${CONTAINER_NAME} --restart unless-stopped \
--memory=512m --cpus=1 \
--entrypoint sleep \
${IMAGE} infinity`,
{ stdio: "inherit" }
);

// Brief pause to allow Docker to initialize the process
await new Promise(r => setTimeout(r, 300));
}
} catch (e: any) {
if (e.code === 'EEXIST') {
// Another process is handling startup, wait briefly
await new Promise(r => setTimeout(r, 500));
} else {
throw e;
}
} finally {
if (lockFd !== null) {
fs.closeSync(lockFd);
try { fs.unlinkSync(LOCK_FILE); } catch {}
}
}

// 2. Connect to the MCP server inside the container
// This allows the local agent/IDE to communicate with the containerized Bun process.
const proc = spawn("docker", ["exec", "-i", CONTAINER_NAME, "bun", "/app/src/index.ts"], {
stdio: "inherit"
});

proc.on("exit", (code) => process.exit(code || 0));
proc.on("error", (err) => {
console.error("Failed to bridge to containerized MCP:", err.message);
process.exit(1);
});

} catch (error: any) {
console.error("Critical error starting Hyperstack MCP:", error.message);
process.exit(1);
}
}

startMcp();
Loading