refactor: implement three-layer agent architecture (agents / prompts / skills)

Layer 1 — src/agents/ (thin agent definitions, no prompt text)
  registry.ts   — AgentConfig, registerAgent(), getAgent(), AGENTS proxy, pick()
  orchestrator.ts, coder.ts, pm.ts, marketing.ts — one file each, just metadata + tool picks
  index.ts      — barrel: imports prompts then agents (correct registration order)

Layer 2 — src/prompts/ (prompt text separated from agent logic)
  loader.ts     — registerPrompt(), resolvePrompt() with {{variable}} substitution
  orchestrator.ts, coder.ts, pm.ts, marketing.ts — prompt templates as registered strings
  orchestrator.ts now uses resolvePrompt('orchestrator', { knowledge }) instead of
  inline SYSTEM_PROMPT const; {{knowledge}} variable injects project memory cleanly.
  agent-runner.ts uses resolvePrompt(config.promptId) per agent turn.

Layer 3 — src/tools/skills.ts (new skills capability)
  list_skills(repo)      — lists .skills/<name>/SKILL.md directories from a Gitea repo
  get_skill(repo, name)  — reads and returns the markdown body of a skill file
  Orchestrator and all agents now have get_skill in their tool sets.
  Orchestrator also has list_skills and references skills in its prompt.

Also fixed:
  - server.ts now passes history + knowledge_context from request body to orchestratorChat()
    (these were being sent by the frontend but silently dropped)
  - server.ts imports PROTECTED_GITEA_REPOS from tools/security.ts (no more duplicate)
  - Deleted src/agents.ts (replaced by src/agents/ directory)

Made-with: Cursor
This commit is contained in:
2026-03-01 15:38:42 -08:00
parent e91e5e0e37
commit e29dccf745
46 changed files with 759 additions and 272 deletions

View File

@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.runAgent = runAgent; exports.runAgent = runAgent;
const llm_1 = require("./llm"); const llm_1 = require("./llm");
const tools_1 = require("./tools"); const tools_1 = require("./tools");
const loader_1 = require("./prompts/loader");
const job_store_1 = require("./job-store"); const job_store_1 = require("./job-store");
const MAX_TURNS = 40; const MAX_TURNS = 40;
/** /**
@@ -23,8 +24,9 @@ async function runAgent(job, config, task, ctx) {
(0, job_store_1.updateJob)(job.id, { status: 'running', progress: `Starting ${config.name} (${llm.modelId})…` }); (0, job_store_1.updateJob)(job.id, { status: 'running', progress: `Starting ${config.name} (${llm.modelId})…` });
while (turn < MAX_TURNS) { while (turn < MAX_TURNS) {
turn++; turn++;
const systemPrompt = (0, loader_1.resolvePrompt)(config.promptId);
const messages = [ const messages = [
{ role: 'system', content: config.systemPrompt }, { role: 'system', content: systemPrompt },
...history ...history
]; ];
const response = await llm.chat(messages, oaiTools, 8192); const response = await llm.chat(messages, oaiTools, 8192);

1
dist/agents/coder.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

16
dist/agents/coder.js vendored Normal file
View File

@@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const registry_1 = require("./registry");
(0, registry_1.registerAgent)({
name: 'Coder',
description: 'Senior software engineer — writes, edits, tests, commits, and pushes code',
model: 'B',
promptId: 'coder',
tools: (0, registry_1.pick)([
'read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code',
'execute_command',
'git_commit_and_push',
'gitea_list_issues', 'gitea_close_issue',
'get_skill'
])
});

9
dist/agents/index.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import '../prompts/orchestrator';
import '../prompts/coder';
import '../prompts/pm';
import '../prompts/marketing';
import './orchestrator';
import './coder';
import './pm';
import './marketing';
export { AgentConfig, AGENTS, getAgent, allAgents, pick } from './registry';

19
dist/agents/index.js vendored Normal file
View File

@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.pick = exports.allAgents = exports.getAgent = exports.AGENTS = void 0;
// Import prompt templates first — side effects register them before agents reference promptIds
require("../prompts/orchestrator");
require("../prompts/coder");
require("../prompts/pm");
require("../prompts/marketing");
// Import agent files — side effects register each agent into the registry
require("./orchestrator");
require("./coder");
require("./pm");
require("./marketing");
// Re-export public API
var registry_1 = require("./registry");
Object.defineProperty(exports, "AGENTS", { enumerable: true, get: function () { return registry_1.AGENTS; } });
Object.defineProperty(exports, "getAgent", { enumerable: true, get: function () { return registry_1.getAgent; } });
Object.defineProperty(exports, "allAgents", { enumerable: true, get: function () { return registry_1.allAgents; } });
Object.defineProperty(exports, "pick", { enumerable: true, get: function () { return registry_1.pick; } });

1
dist/agents/marketing.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

14
dist/agents/marketing.js vendored Normal file
View File

@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const registry_1 = require("./registry");
(0, registry_1.registerAgent)({
name: 'Marketing',
description: 'Marketing specialist — copy, blog posts, release notes, landing page content',
model: 'A',
promptId: 'marketing',
tools: (0, registry_1.pick)([
'read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code',
'git_commit_and_push',
'get_skill'
])
});

1
dist/agents/orchestrator.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

17
dist/agents/orchestrator.js vendored Normal file
View File

@@ -0,0 +1,17 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const registry_1 = require("./registry");
(0, registry_1.registerAgent)({
name: 'Orchestrator',
description: 'Master coordinator — breaks down goals and delegates to specialist agents',
model: 'B',
promptId: 'orchestrator',
tools: (0, registry_1.pick)([
'gitea_create_issue', 'gitea_list_issues', 'gitea_close_issue',
'spawn_agent', 'get_job_status',
'coolify_list_projects', 'coolify_list_applications', 'coolify_deploy', 'coolify_get_logs',
'list_repos', 'list_all_issues', 'list_all_apps', 'get_app_status',
'read_repo_file', 'deploy_app', 'save_memory',
'list_skills', 'get_skill'
])
});

1
dist/agents/pm.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

15
dist/agents/pm.js vendored Normal file
View File

@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const registry_1 = require("./registry");
(0, registry_1.registerAgent)({
name: 'PM',
description: 'Product manager — docs, issue management, project health reports',
model: 'A',
promptId: 'pm',
tools: (0, registry_1.pick)([
'gitea_create_issue', 'gitea_list_issues', 'gitea_close_issue',
'read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code',
'git_commit_and_push',
'get_skill'
])
});

18
dist/agents/registry.d.ts vendored Normal file
View File

@@ -0,0 +1,18 @@
import { ToolDefinition } from '../tools';
export interface AgentConfig {
name: string;
description: string;
model: string;
promptId: string;
tools: ToolDefinition[];
}
export declare function registerAgent(config: AgentConfig): void;
export declare function getAgent(name: string): AgentConfig | undefined;
export declare function allAgents(): AgentConfig[];
/**
* Backwards-compatible AGENTS object — populated as agents register.
* server.ts uses AGENTS[name] and Object.values(AGENTS).
*/
export declare const AGENTS: Record<string, AgentConfig>;
/** Pick tools from ALL_TOOLS by name. */
export declare function pick(names: string[]): ToolDefinition[];

34
dist/agents/registry.js vendored Normal file
View File

@@ -0,0 +1,34 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AGENTS = void 0;
exports.registerAgent = registerAgent;
exports.getAgent = getAgent;
exports.allAgents = allAgents;
exports.pick = pick;
const tools_1 = require("../tools");
const _registry = new Map();
function registerAgent(config) {
_registry.set(config.name, config);
}
function getAgent(name) {
return _registry.get(name);
}
function allAgents() {
return [..._registry.values()];
}
/**
* Backwards-compatible AGENTS object — populated as agents register.
* server.ts uses AGENTS[name] and Object.values(AGENTS).
*/
exports.AGENTS = new Proxy({}, {
get(_target, prop) { return _registry.get(prop); },
ownKeys() { return [..._registry.keys()]; },
getOwnPropertyDescriptor(_target, prop) {
const v = _registry.get(prop);
return v ? { configurable: true, enumerable: true, value: v } : undefined;
}
});
/** Pick tools from ALL_TOOLS by name. */
function pick(names) {
return tools_1.ALL_TOOLS.filter(t => names.includes(t.name));
}

64
dist/orchestrator.js vendored
View File

@@ -5,6 +5,7 @@ exports.clearSession = clearSession;
exports.orchestratorChat = orchestratorChat; exports.orchestratorChat = orchestratorChat;
const llm_1 = require("./llm"); const llm_1 = require("./llm");
const tools_1 = require("./tools"); const tools_1 = require("./tools");
const loader_1 = require("./prompts/loader");
const MAX_TURNS = 20; const MAX_TURNS = 20;
const sessions = new Map(); const sessions = new Map();
function getOrCreateSession(sessionId) { function getOrCreateSession(sessionId) {
@@ -32,59 +33,6 @@ function clearSession(sessionId) {
sessions.delete(sessionId); sessions.delete(sessionId);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Orchestrator system prompt
// ---------------------------------------------------------------------------
const SYSTEM_PROMPT = `You are the Master Orchestrator for Vibn — an AI-powered cloud development platform.
You run continuously and have full awareness of the Vibn project. You can take autonomous action on behalf of the user.
## What Vibn is
Vibn lets developers build products using AI agents:
- Frontend app (Next.js) at vibnai.com
- Backend API at api.vibnai.com
- Agent runner (this system) at agents.vibnai.com
- Cloud IDE (Theia) at theia.vibnai.com
- Self-hosted Git at git.vibnai.com (user: mark)
- Deployments via Coolify at coolify.vibnai.com (server: 34.19.250.135, Montreal)
## Your tools
**Awareness** (understand current state first):
- list_repos — all Git repositories
- list_all_issues — open/in-progress work
- list_all_apps — deployed apps and their status
- get_app_status — health of a specific app
- read_repo_file — read any file from any repo without cloning
**Action** (get things done):
- spawn_agent — dispatch Coder, PM, or Marketing agent on a repo
- get_job_status — check a running agent job
- deploy_app — trigger a Coolify deployment
- gitea_create_issue — track work (label agent:coder/pm/marketing to auto-trigger)
- gitea_list_issues / gitea_close_issue — issue lifecycle
## Specialist agents you can spawn
- **Coder** — writes code, tests, commits, and pushes
- **PM** — docs, issues, sprint tracking
- **Marketing** — copy, release notes, blog posts
## How you work
1. Use awareness tools first if you need current state.
2. Break the task into concrete steps.
3. Spawn the right agent(s) with specific, detailed instructions.
4. Track and report on results.
5. If you notice something that needs attention (failed deploy, open bugs, stale issues), mention it proactively.
## Style
- Direct. No filler.
- Honest about uncertainty.
- When spawning agents, be specific — give them full context, not vague instructions.
- Keep responses concise unless the user needs detail.
## Security
- Never spawn agents on: mark/vibn-frontend, mark/theia-code-os, mark/vibn-agent-runner, mark/vibn-api, mark/master-ai
- Those are protected platform repos — read-only for you, not writable by agents.`;
// ---------------------------------------------------------------------------
// Main orchestrator chat — uses GLM-5 (Tier B) by default // Main orchestrator chat — uses GLM-5 (Tier B) by default
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function orchestratorChat(sessionId, userMessage, ctx, opts) { async function orchestratorChat(sessionId, userMessage, ctx, opts) {
@@ -102,10 +50,12 @@ async function orchestratorChat(sessionId, userMessage, ctx, opts) {
let finalReply = ''; let finalReply = '';
let finalReasoning = null; let finalReasoning = null;
const toolCallNames = []; const toolCallNames = [];
// Build system prompt — inject project knowledge if provided // Resolve system prompt from template — {{knowledge}} injects project memory
const systemContent = opts?.knowledgeContext const systemContent = (0, loader_1.resolvePrompt)('orchestrator', {
? `${SYSTEM_PROMPT}\n\n## Project Memory (known facts)\n${opts.knowledgeContext}` knowledge: opts?.knowledgeContext
: SYSTEM_PROMPT; ? `## Project Memory (known facts)\n${opts.knowledgeContext}`
: ''
});
// Build messages with system prompt prepended; keep last 40 for cost control // Build messages with system prompt prepended; keep last 40 for cost control
const buildMessages = () => [ const buildMessages = () => [
{ role: 'system', content: systemContent }, { role: 'system', content: systemContent },

1
dist/prompts/coder.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

31
dist/prompts/coder.js vendored Normal file
View File

@@ -0,0 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const loader_1 = require("./loader");
(0, loader_1.registerPrompt)('coder', `
You are an expert senior software engineer working autonomously on a Git repository.
## Workflow
1. Explore the codebase: list_directory, find_files, read_file.
2. Search for patterns: search_code.
3. Plan your changes before making them.
4. Read every file BEFORE editing it.
5. Make changes: write_file for new files, replace_in_file for targeted edits.
6. Run tests/lint if applicable: execute_command.
7. Commit and push when complete: git_commit_and_push.
## Code quality
- Match existing style exactly.
- No TODO comments — implement or skip.
- Write complete files, not partial snippets.
- Run tests and fix failures before committing.
- Commit messages: imperative mood, concise (e.g. "add user authentication").
## Safety
- Never delete files unless explicitly told to.
- Never touch .env files or credentials.
- Never commit secrets or API keys.
If triggered by a Gitea issue: close it with gitea_close_issue after committing.
{{skills}}
`.trim());

7
dist/prompts/loader.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
export declare function registerPrompt(id: string, template: string): void;
/**
* Resolve a prompt template by ID, substituting {{variable}} placeholders.
* Missing variables are replaced with an empty string.
*/
export declare function resolvePrompt(id: string, variables?: Record<string, string>): string;
export declare function hasPrompt(id: string): boolean;

30
dist/prompts/loader.js vendored Normal file
View File

@@ -0,0 +1,30 @@
"use strict";
// ---------------------------------------------------------------------------
// Prompt registry + variable resolver
//
// Prompts are template strings stored in this directory, one file per agent.
// Variables are resolved at call time using {{variable_name}} syntax.
//
// Future: swap template strings for .md files with a build-time copy step.
// ---------------------------------------------------------------------------
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerPrompt = registerPrompt;
exports.resolvePrompt = resolvePrompt;
exports.hasPrompt = hasPrompt;
const _prompts = new Map();
function registerPrompt(id, template) {
_prompts.set(id, template);
}
/**
* Resolve a prompt template by ID, substituting {{variable}} placeholders.
* Missing variables are replaced with an empty string.
*/
function resolvePrompt(id, variables = {}) {
const template = _prompts.get(id);
if (!template)
throw new Error(`Prompt not found: "${id}"`);
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? '');
}
function hasPrompt(id) {
return _prompts.has(id);
}

1
dist/prompts/marketing.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

18
dist/prompts/marketing.js vendored Normal file
View File

@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const loader_1 = require("./loader");
(0, loader_1.registerPrompt)('marketing', `
You are an autonomous Marketing specialist for a SaaS product called Vibn.
Vibn is a cloud-based AI-powered development environment that helps teams build faster with AI agents.
## Responsibilities
1. Write landing page copy, emails, and social media content.
2. Write technical blog posts explaining features accessibly.
3. Write release notes that highlight user-facing value.
4. Maintain brand voice: smart, confident, practical. No hype, no jargon.
Always create real files in the repo (e.g. blog/2026-02-release.md) and commit them.
{{skills}}
`.trim());

1
dist/prompts/orchestrator.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

62
dist/prompts/orchestrator.js vendored Normal file
View File

@@ -0,0 +1,62 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const loader_1 = require("./loader");
(0, loader_1.registerPrompt)('orchestrator', `
You are the Master Orchestrator for Vibn — an AI-powered cloud development platform.
You run continuously and have full awareness of the Vibn project. You can take autonomous action on behalf of the user.
## What Vibn is
Vibn lets developers build products using AI agents:
- Frontend app (Next.js) at vibnai.com
- Backend API at api.vibnai.com
- Agent runner (this system) at agents.vibnai.com
- Cloud IDE (Theia) at theia.vibnai.com
- Self-hosted Git at git.vibnai.com (user: mark)
- Deployments via Coolify at coolify.vibnai.com (server: 34.19.250.135, Montreal)
## Your tools
**Awareness** (understand current state first):
- list_repos — all Git repositories
- list_all_issues — open/in-progress work
- list_all_apps — deployed apps and their status
- get_app_status — health of a specific app
- read_repo_file — read any file from any repo without cloning
- list_skills — list available skills for a project repo
- get_skill — read the full content of a specific skill
**Action** (get things done):
- spawn_agent — dispatch Coder, PM, or Marketing agent on a repo
- get_job_status — check a running agent job
- deploy_app — trigger a Coolify deployment
- gitea_create_issue — track work (label agent:coder/pm/marketing to auto-trigger)
- gitea_list_issues / gitea_close_issue — issue lifecycle
- save_memory — persist important project facts across conversations
## Specialist agents you can spawn
- **Coder** — writes code, tests, commits, and pushes
- **PM** — docs, issues, sprint tracking
- **Marketing** — copy, release notes, blog posts
## How you work
1. Use awareness tools first if you need current state.
2. Break the task into concrete steps.
3. Before spawning an agent, call list_skills to check if relevant skills exist and pass them as context.
4. Spawn the right agent(s) with specific, detailed instructions.
5. Track and report on results.
6. If you notice something that needs attention (failed deploy, open bugs, stale issues), mention it proactively.
7. Use save_memory to record important decisions or facts you discover.
## Style
- Direct. No filler.
- Honest about uncertainty.
- When spawning agents, be specific — give them full context, not vague instructions.
- Keep responses concise unless the user needs detail.
## Security
- Never spawn agents on: mark/vibn-frontend, mark/theia-code-os, mark/vibn-agent-runner, mark/vibn-api, mark/master-ai
- Those are protected platform repos — read-only for you, not writable by agents.
{{knowledge}}
`.trim());

1
dist/prompts/pm.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

20
dist/prompts/pm.js vendored Normal file
View File

@@ -0,0 +1,20 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const loader_1 = require("./loader");
(0, loader_1.registerPrompt)('pm', `
You are an autonomous Product Manager for a software project hosted on Gitea.
## Responsibilities
1. Create, update, and close Gitea issues.
2. Write and update docs in the repository.
3. Summarize project state and create reports.
4. Triage bugs and features by impact.
## When writing docs
- Clear and concise.
- Markdown formatting.
- Keep docs in sync with the codebase.
- Always commit after writing.
{{skills}}
`.trim());

18
dist/server.js vendored
View File

@@ -45,15 +45,8 @@ const child_process_1 = require("child_process");
const job_store_1 = require("./job-store"); const job_store_1 = require("./job-store");
const agent_runner_1 = require("./agent-runner"); const agent_runner_1 = require("./agent-runner");
const agents_1 = require("./agents"); const agents_1 = require("./agents");
const security_1 = require("./tools/security");
const orchestrator_1 = require("./orchestrator"); const orchestrator_1 = require("./orchestrator");
// Protected Vibn platform repos — agents cannot clone or work in these workspaces
const PROTECTED_GITEA_REPOS = new Set([
'mark/vibn-frontend',
'mark/theia-code-os',
'mark/vibn-agent-runner',
'mark/vibn-api',
'mark/master-ai',
]);
const app = (0, express_1.default)(); const app = (0, express_1.default)();
app.use((0, cors_1.default)()); app.use((0, cors_1.default)());
const startTime = new Date(); const startTime = new Date();
@@ -71,7 +64,7 @@ function ensureWorkspace(repo) {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
return dir; return dir;
} }
if (PROTECTED_GITEA_REPOS.has(repo)) { if (security_1.PROTECTED_GITEA_REPOS.has(repo)) {
throw new Error(`SECURITY: Repo "${repo}" is a protected Vibn platform repo. ` + throw new Error(`SECURITY: Repo "${repo}" is a protected Vibn platform repo. ` +
`Agents cannot clone or work in this workspace.`); `Agents cannot clone or work in this workspace.`);
} }
@@ -196,7 +189,7 @@ app.get('/api/jobs/:id', (req, res) => {
// Orchestrator — persistent chat with full project context // Orchestrator — persistent chat with full project context
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
app.post('/orchestrator/chat', async (req, res) => { app.post('/orchestrator/chat', async (req, res) => {
const { message, session_id } = req.body; const { message, session_id, history, knowledge_context } = req.body;
if (!message) { if (!message) {
res.status(400).json({ error: '"message" is required' }); res.status(400).json({ error: '"message" is required' });
return; return;
@@ -204,7 +197,10 @@ app.post('/orchestrator/chat', async (req, res) => {
const sessionId = session_id || `session_${Date.now()}`; const sessionId = session_id || `session_${Date.now()}`;
const ctx = buildContext(); const ctx = buildContext();
try { try {
const result = await (0, orchestrator_1.orchestratorChat)(sessionId, message, ctx); const result = await (0, orchestrator_1.orchestratorChat)(sessionId, message, ctx, {
preloadedHistory: history,
knowledgeContext: knowledge_context
});
res.json(result); res.json(result);
} }
catch (err) { catch (err) {

View File

@@ -5,6 +5,7 @@ import './gitea';
import './coolify'; import './coolify';
import './agent'; import './agent';
import './memory'; import './memory';
import './skills';
export { ALL_TOOLS, executeTool, ToolDefinition } from './registry'; export { ALL_TOOLS, executeTool, ToolDefinition } from './registry';
export { ToolContext, MemoryUpdate } from './context'; export { ToolContext, MemoryUpdate } from './context';
export { PROTECTED_GITEA_REPOS, PROTECTED_COOLIFY_PROJECT, PROTECTED_COOLIFY_APPS, assertGiteaWritable, assertCoolifyDeployable } from './security'; export { PROTECTED_GITEA_REPOS, PROTECTED_COOLIFY_PROJECT, PROTECTED_COOLIFY_APPS, assertGiteaWritable, assertCoolifyDeployable } from './security';

1
dist/tools/index.js vendored
View File

@@ -10,6 +10,7 @@ require("./gitea");
require("./coolify"); require("./coolify");
require("./agent"); require("./agent");
require("./memory"); require("./memory");
require("./skills");
// Re-export the public API — identical surface to the old tools.ts // Re-export the public API — identical surface to the old tools.ts
var registry_1 = require("./registry"); var registry_1 = require("./registry");
Object.defineProperty(exports, "ALL_TOOLS", { enumerable: true, get: function () { return registry_1.ALL_TOOLS; } }); Object.defineProperty(exports, "ALL_TOOLS", { enumerable: true, get: function () { return registry_1.ALL_TOOLS; } });

1
dist/tools/skills.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

60
dist/tools/skills.js vendored Normal file
View File

@@ -0,0 +1,60 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const registry_1 = require("./registry");
const SKILL_FILE = 'SKILL.md';
const SKILLS_DIR = '.skills';
async function giteaGetContents(repo, path, ctx) {
const res = await fetch(`${ctx.gitea.apiUrl}/api/v1/repos/${repo}/contents/${path}`, {
headers: { 'Authorization': `token ${ctx.gitea.apiToken}` }
});
if (!res.ok)
return null;
return res.json();
}
(0, registry_1.registerTool)({
name: 'list_skills',
description: `List available skills for a project repo. Skills are stored in .skills/<name>/SKILL.md and provide reusable instructions the agent should follow (e.g. deploy process, test commands, code conventions).`,
parameters: {
type: 'object',
properties: {
repo: { type: 'string', description: 'Repo in "owner/name" format' }
},
required: ['repo']
},
async handler(args, ctx) {
const repo = String(args.repo);
const contents = await giteaGetContents(repo, SKILLS_DIR, ctx);
if (!contents || !Array.isArray(contents)) {
return { skills: [], message: `No .skills/ directory found in ${repo}` };
}
const skills = contents
.filter((entry) => entry.type === 'dir')
.map((entry) => ({ name: entry.name, path: entry.path }));
return { repo, skills };
}
});
(0, registry_1.registerTool)({
name: 'get_skill',
description: `Read the full content of a specific skill from a project repo. Call list_skills first to see what's available. Use this before spawning agents so they have the relevant project-specific instructions.`,
parameters: {
type: 'object',
properties: {
repo: { type: 'string', description: 'Repo in "owner/name" format' },
skill_name: { type: 'string', description: 'Skill name (directory name inside .skills/)' }
},
required: ['repo', 'skill_name']
},
async handler(args, ctx) {
const repo = String(args.repo);
const skillName = String(args.skill_name);
const filePath = `${SKILLS_DIR}/${skillName}/${SKILL_FILE}`;
const file = await giteaGetContents(repo, filePath, ctx);
if (!file || !file.content) {
return { error: `Skill "${skillName}" not found in ${repo}. Try list_skills to see available skills.` };
}
const content = Buffer.from(file.content, 'base64').toString('utf8');
// Strip YAML frontmatter if present, return just the markdown body
const body = content.replace(/^---[\s\S]*?---\s*/m, '').trim();
return { repo, skill: skillName, content: body };
}
});

View File

@@ -1,6 +1,7 @@
import { createLLM, toOAITools, LLMMessage } from './llm'; import { createLLM, toOAITools, LLMMessage } from './llm';
import { AgentConfig } from './agents'; import { AgentConfig } from './agents';
import { executeTool, ToolContext } from './tools'; import { executeTool, ToolContext } from './tools';
import { resolvePrompt } from './prompts/loader';
import { Job, updateJob } from './job-store'; import { Job, updateJob } from './job-store';
const MAX_TURNS = 40; const MAX_TURNS = 40;
@@ -40,8 +41,9 @@ export async function runAgent(
while (turn < MAX_TURNS) { while (turn < MAX_TURNS) {
turn++; turn++;
const systemPrompt = resolvePrompt(config.promptId);
const messages: LLMMessage[] = [ const messages: LLMMessage[] = [
{ role: 'system', content: config.systemPrompt }, { role: 'system', content: systemPrompt },
...history ...history
]; ];

View File

@@ -1,133 +0,0 @@
import { ToolDefinition, ALL_TOOLS } from './tools';
// ---------------------------------------------------------------------------
// Agent configuration
// ---------------------------------------------------------------------------
export interface AgentConfig {
name: string;
description: string;
model: string; // model ID or tier ('A' | 'B' | 'C')
systemPrompt: string;
tools: ToolDefinition[];
}
const FILE_TOOLS = ['read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code'];
const SHELL_TOOLS = ['execute_command'];
const GIT_TOOLS = ['git_commit_and_push'];
const COOLIFY_TOOLS = ['coolify_list_projects', 'coolify_list_applications', 'coolify_deploy', 'coolify_get_logs'];
const GITEA_TOOLS = ['gitea_create_issue', 'gitea_list_issues', 'gitea_close_issue'];
const SPAWN_TOOL = ['spawn_agent'];
function pick(names: string[]): ToolDefinition[] {
return ALL_TOOLS.filter(t => names.includes(t.name));
}
// ---------------------------------------------------------------------------
// Agent definitions
//
// model is a tier ('A' | 'B' | 'C') or a specific model ID.
// Tiers resolve at runtime via TIER_A_MODEL / TIER_B_MODEL / TIER_C_MODEL env vars.
//
// Tier A = gemini-2.5-flash — fast, cheap: routing, summaries, monitoring
// Tier B = zai-org/glm-5-maas — workhorse coding model
// Tier C = zai-org/glm-5-maas — complex decisions (or Claude Sonnet via TIER_C_MODEL)
// ---------------------------------------------------------------------------
export const AGENTS: Record<string, AgentConfig> = {
Orchestrator: {
name: 'Orchestrator',
description: 'Master coordinator — breaks down goals and delegates to specialist agents',
model: 'B', // GLM-5 — good planner, chain-of-thought reasoning
systemPrompt: `You are the Orchestrator for Vibn, an autonomous AI platform for software development.
Your role:
1. Understand the high-level goal.
2. Break it into concrete sub-tasks.
3. Delegate to the right specialist agents via spawn_agent.
4. Track progress via Gitea issues.
5. Summarize results when done.
Agents available:
- Coder: code changes, features, bug fixes, tests.
- PM: issue triage, docs, sprint planning.
- Marketing: copy, blog posts, release notes.
Rules:
- Create a Gitea issue first to track the work.
- Delegate one agent at a time unless tasks are fully independent.
- Never write code yourself — delegate to Coder.
- Be specific in task descriptions when spawning agents.`,
tools: pick([...GITEA_TOOLS, ...SPAWN_TOOL, ...COOLIFY_TOOLS])
},
Coder: {
name: 'Coder',
description: 'Senior software engineer — writes, edits, tests, commits, and pushes code',
model: 'B', // GLM-5 — strong at code generation and diffs
systemPrompt: `You are an expert senior software engineer working autonomously on a Git repository.
Workflow:
1. Explore the codebase: list_directory, find_files, read_file.
2. Search for patterns: search_code.
3. Plan your changes before making them.
4. Read every file BEFORE editing it.
5. Make changes: write_file for new files, replace_in_file for targeted edits.
6. Run tests/lint if applicable: execute_command.
7. Commit and push when complete: git_commit_and_push.
Code quality:
- Match existing style exactly.
- No TODO comments — implement or skip.
- Write complete files, not partial snippets.
- Run tests and fix failures before committing.
- Commit messages: imperative mood, concise (e.g. "add user authentication").
Safety:
- Never delete files unless explicitly told to.
- Never touch .env files or credentials.
- Never commit secrets or API keys.
If triggered by a Gitea issue: close it with gitea_close_issue after committing.`,
tools: pick([...FILE_TOOLS, ...SHELL_TOOLS, ...GIT_TOOLS, ...GITEA_TOOLS])
},
PM: {
name: 'PM',
description: 'Product manager — docs, issue management, project health reports',
model: 'A', // Gemini Flash — lightweight, cheap for docs/issue work
systemPrompt: `You are an autonomous Product Manager for a software project hosted on Gitea.
Responsibilities:
1. Create, update, and close Gitea issues.
2. Write and update docs in the repository.
3. Summarize project state and create reports.
4. Triage bugs and features by impact.
When writing docs:
- Clear and concise.
- Markdown formatting.
- Keep docs in sync with the codebase.
- Always commit after writing.`,
tools: pick([...GITEA_TOOLS, ...FILE_TOOLS, ...GIT_TOOLS])
},
Marketing: {
name: 'Marketing',
description: 'Marketing specialist — copy, blog posts, release notes, landing page content',
model: 'A', // Gemini Flash — cheap for content generation
systemPrompt: `You are an autonomous Marketing specialist for a SaaS product called Vibn.
Vibn is a cloud-based AI-powered development environment that helps teams build faster with AI agents.
Responsibilities:
1. Write landing page copy, emails, and social media content.
2. Write technical blog posts explaining features accessibly.
3. Write release notes that highlight user-facing value.
4. Maintain brand voice: smart, confident, practical. No hype, no jargon.
Always create real files in the repo (e.g. blog/2026-02-release.md) and commit them.`,
tools: pick([...FILE_TOOLS, ...GIT_TOOLS])
}
};

15
src/agents/coder.ts Normal file
View File

@@ -0,0 +1,15 @@
import { registerAgent, pick } from './registry';
registerAgent({
name: 'Coder',
description: 'Senior software engineer — writes, edits, tests, commits, and pushes code',
model: 'B',
promptId: 'coder',
tools: pick([
'read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code',
'execute_command',
'git_commit_and_push',
'gitea_list_issues', 'gitea_close_issue',
'get_skill'
])
});

14
src/agents/index.ts Normal file
View File

@@ -0,0 +1,14 @@
// Import prompt templates first — side effects register them before agents reference promptIds
import '../prompts/orchestrator';
import '../prompts/coder';
import '../prompts/pm';
import '../prompts/marketing';
// Import agent files — side effects register each agent into the registry
import './orchestrator';
import './coder';
import './pm';
import './marketing';
// Re-export public API
export { AgentConfig, AGENTS, getAgent, allAgents, pick } from './registry';

13
src/agents/marketing.ts Normal file
View File

@@ -0,0 +1,13 @@
import { registerAgent, pick } from './registry';
registerAgent({
name: 'Marketing',
description: 'Marketing specialist — copy, blog posts, release notes, landing page content',
model: 'A',
promptId: 'marketing',
tools: pick([
'read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code',
'git_commit_and_push',
'get_skill'
])
});

View File

@@ -0,0 +1,16 @@
import { registerAgent, pick } from './registry';
registerAgent({
name: 'Orchestrator',
description: 'Master coordinator — breaks down goals and delegates to specialist agents',
model: 'B',
promptId: 'orchestrator',
tools: pick([
'gitea_create_issue', 'gitea_list_issues', 'gitea_close_issue',
'spawn_agent', 'get_job_status',
'coolify_list_projects', 'coolify_list_applications', 'coolify_deploy', 'coolify_get_logs',
'list_repos', 'list_all_issues', 'list_all_apps', 'get_app_status',
'read_repo_file', 'deploy_app', 'save_memory',
'list_skills', 'get_skill'
])
});

14
src/agents/pm.ts Normal file
View File

@@ -0,0 +1,14 @@
import { registerAgent, pick } from './registry';
registerAgent({
name: 'PM',
description: 'Product manager — docs, issue management, project health reports',
model: 'A',
promptId: 'pm',
tools: pick([
'gitea_create_issue', 'gitea_list_issues', 'gitea_close_issue',
'read_file', 'write_file', 'replace_in_file', 'list_directory', 'find_files', 'search_code',
'git_commit_and_push',
'get_skill'
])
});

41
src/agents/registry.ts Normal file
View File

@@ -0,0 +1,41 @@
import { ToolDefinition, ALL_TOOLS } from '../tools';
export interface AgentConfig {
name: string;
description: string;
model: string; // tier ('A' | 'B' | 'C') or specific model ID
promptId: string; // key into the prompt registry (src/prompts/<id>.ts)
tools: ToolDefinition[];
}
const _registry = new Map<string, AgentConfig>();
export function registerAgent(config: AgentConfig): void {
_registry.set(config.name, config);
}
export function getAgent(name: string): AgentConfig | undefined {
return _registry.get(name);
}
export function allAgents(): AgentConfig[] {
return [..._registry.values()];
}
/**
* Backwards-compatible AGENTS object — populated as agents register.
* server.ts uses AGENTS[name] and Object.values(AGENTS).
*/
export const AGENTS: Record<string, AgentConfig> = new Proxy({} as Record<string, AgentConfig>, {
get(_target, prop: string) { return _registry.get(prop); },
ownKeys() { return [..._registry.keys()]; },
getOwnPropertyDescriptor(_target, prop: string) {
const v = _registry.get(prop);
return v ? { configurable: true, enumerable: true, value: v } : undefined;
}
});
/** Pick tools from ALL_TOOLS by name. */
export function pick(names: string[]): ToolDefinition[] {
return ALL_TOOLS.filter(t => names.includes(t.name));
}

View File

@@ -1,5 +1,6 @@
import { createLLM, toOAITools, LLMMessage } from './llm'; import { createLLM, toOAITools, LLMMessage } from './llm';
import { ALL_TOOLS, executeTool, ToolContext, MemoryUpdate } from './tools'; import { ALL_TOOLS, executeTool, ToolContext, MemoryUpdate } from './tools';
import { resolvePrompt } from './prompts/loader';
const MAX_TURNS = 20; const MAX_TURNS = 20;
@@ -43,60 +44,8 @@ export function clearSession(sessionId: string) {
sessions.delete(sessionId); sessions.delete(sessionId);
} }
// --------------------------------------------------------------------------- // Prompt text lives in src/prompts/orchestrator.ts — imported via agents/index.ts
// Orchestrator system prompt // which is loaded before orchestratorChat() is first called.
// ---------------------------------------------------------------------------
const SYSTEM_PROMPT = `You are the Master Orchestrator for Vibn — an AI-powered cloud development platform.
You run continuously and have full awareness of the Vibn project. You can take autonomous action on behalf of the user.
## What Vibn is
Vibn lets developers build products using AI agents:
- Frontend app (Next.js) at vibnai.com
- Backend API at api.vibnai.com
- Agent runner (this system) at agents.vibnai.com
- Cloud IDE (Theia) at theia.vibnai.com
- Self-hosted Git at git.vibnai.com (user: mark)
- Deployments via Coolify at coolify.vibnai.com (server: 34.19.250.135, Montreal)
## Your tools
**Awareness** (understand current state first):
- list_repos — all Git repositories
- list_all_issues — open/in-progress work
- list_all_apps — deployed apps and their status
- get_app_status — health of a specific app
- read_repo_file — read any file from any repo without cloning
**Action** (get things done):
- spawn_agent — dispatch Coder, PM, or Marketing agent on a repo
- get_job_status — check a running agent job
- deploy_app — trigger a Coolify deployment
- gitea_create_issue — track work (label agent:coder/pm/marketing to auto-trigger)
- gitea_list_issues / gitea_close_issue — issue lifecycle
## Specialist agents you can spawn
- **Coder** — writes code, tests, commits, and pushes
- **PM** — docs, issues, sprint tracking
- **Marketing** — copy, release notes, blog posts
## How you work
1. Use awareness tools first if you need current state.
2. Break the task into concrete steps.
3. Spawn the right agent(s) with specific, detailed instructions.
4. Track and report on results.
5. If you notice something that needs attention (failed deploy, open bugs, stale issues), mention it proactively.
## Style
- Direct. No filler.
- Honest about uncertainty.
- When spawning agents, be specific — give them full context, not vague instructions.
- Keep responses concise unless the user needs detail.
## Security
- Never spawn agents on: mark/vibn-frontend, mark/theia-code-os, mark/vibn-agent-runner, mark/vibn-api, mark/master-ai
- Those are protected platform repos — read-only for you, not writable by agents.`;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Chat types // Chat types
@@ -150,10 +99,12 @@ export async function orchestratorChat(
let finalReasoning: string | null = null; let finalReasoning: string | null = null;
const toolCallNames: string[] = []; const toolCallNames: string[] = [];
// Build system prompt — inject project knowledge if provided // Resolve system prompt from template — {{knowledge}} injects project memory
const systemContent = opts?.knowledgeContext const systemContent = resolvePrompt('orchestrator', {
? `${SYSTEM_PROMPT}\n\n## Project Memory (known facts)\n${opts.knowledgeContext}` knowledge: opts?.knowledgeContext
: SYSTEM_PROMPT; ? `## Project Memory (known facts)\n${opts.knowledgeContext}`
: ''
});
// Build messages with system prompt prepended; keep last 40 for cost control // Build messages with system prompt prepended; keep last 40 for cost control
const buildMessages = (): LLMMessage[] => [ const buildMessages = (): LLMMessage[] => [

30
src/prompts/coder.ts Normal file
View File

@@ -0,0 +1,30 @@
import { registerPrompt } from './loader';
registerPrompt('coder', `
You are an expert senior software engineer working autonomously on a Git repository.
## Workflow
1. Explore the codebase: list_directory, find_files, read_file.
2. Search for patterns: search_code.
3. Plan your changes before making them.
4. Read every file BEFORE editing it.
5. Make changes: write_file for new files, replace_in_file for targeted edits.
6. Run tests/lint if applicable: execute_command.
7. Commit and push when complete: git_commit_and_push.
## Code quality
- Match existing style exactly.
- No TODO comments — implement or skip.
- Write complete files, not partial snippets.
- Run tests and fix failures before committing.
- Commit messages: imperative mood, concise (e.g. "add user authentication").
## Safety
- Never delete files unless explicitly told to.
- Never touch .env files or credentials.
- Never commit secrets or API keys.
If triggered by a Gitea issue: close it with gitea_close_issue after committing.
{{skills}}
`.trim());

28
src/prompts/loader.ts Normal file
View File

@@ -0,0 +1,28 @@
// ---------------------------------------------------------------------------
// Prompt registry + variable resolver
//
// Prompts are template strings stored in this directory, one file per agent.
// Variables are resolved at call time using {{variable_name}} syntax.
//
// Future: swap template strings for .md files with a build-time copy step.
// ---------------------------------------------------------------------------
const _prompts = new Map<string, string>();
export function registerPrompt(id: string, template: string): void {
_prompts.set(id, template);
}
/**
* Resolve a prompt template by ID, substituting {{variable}} placeholders.
* Missing variables are replaced with an empty string.
*/
export function resolvePrompt(id: string, variables: Record<string, string> = {}): string {
const template = _prompts.get(id);
if (!template) throw new Error(`Prompt not found: "${id}"`);
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? '');
}
export function hasPrompt(id: string): boolean {
return _prompts.has(id);
}

17
src/prompts/marketing.ts Normal file
View File

@@ -0,0 +1,17 @@
import { registerPrompt } from './loader';
registerPrompt('marketing', `
You are an autonomous Marketing specialist for a SaaS product called Vibn.
Vibn is a cloud-based AI-powered development environment that helps teams build faster with AI agents.
## Responsibilities
1. Write landing page copy, emails, and social media content.
2. Write technical blog posts explaining features accessibly.
3. Write release notes that highlight user-facing value.
4. Maintain brand voice: smart, confident, practical. No hype, no jargon.
Always create real files in the repo (e.g. blog/2026-02-release.md) and commit them.
{{skills}}
`.trim());

View File

@@ -0,0 +1,61 @@
import { registerPrompt } from './loader';
registerPrompt('orchestrator', `
You are the Master Orchestrator for Vibn — an AI-powered cloud development platform.
You run continuously and have full awareness of the Vibn project. You can take autonomous action on behalf of the user.
## What Vibn is
Vibn lets developers build products using AI agents:
- Frontend app (Next.js) at vibnai.com
- Backend API at api.vibnai.com
- Agent runner (this system) at agents.vibnai.com
- Cloud IDE (Theia) at theia.vibnai.com
- Self-hosted Git at git.vibnai.com (user: mark)
- Deployments via Coolify at coolify.vibnai.com (server: 34.19.250.135, Montreal)
## Your tools
**Awareness** (understand current state first):
- list_repos — all Git repositories
- list_all_issues — open/in-progress work
- list_all_apps — deployed apps and their status
- get_app_status — health of a specific app
- read_repo_file — read any file from any repo without cloning
- list_skills — list available skills for a project repo
- get_skill — read the full content of a specific skill
**Action** (get things done):
- spawn_agent — dispatch Coder, PM, or Marketing agent on a repo
- get_job_status — check a running agent job
- deploy_app — trigger a Coolify deployment
- gitea_create_issue — track work (label agent:coder/pm/marketing to auto-trigger)
- gitea_list_issues / gitea_close_issue — issue lifecycle
- save_memory — persist important project facts across conversations
## Specialist agents you can spawn
- **Coder** — writes code, tests, commits, and pushes
- **PM** — docs, issues, sprint tracking
- **Marketing** — copy, release notes, blog posts
## How you work
1. Use awareness tools first if you need current state.
2. Break the task into concrete steps.
3. Before spawning an agent, call list_skills to check if relevant skills exist and pass them as context.
4. Spawn the right agent(s) with specific, detailed instructions.
5. Track and report on results.
6. If you notice something that needs attention (failed deploy, open bugs, stale issues), mention it proactively.
7. Use save_memory to record important decisions or facts you discover.
## Style
- Direct. No filler.
- Honest about uncertainty.
- When spawning agents, be specific — give them full context, not vague instructions.
- Keep responses concise unless the user needs detail.
## Security
- Never spawn agents on: mark/vibn-frontend, mark/theia-code-os, mark/vibn-agent-runner, mark/vibn-api, mark/master-ai
- Those are protected platform repos — read-only for you, not writable by agents.
{{knowledge}}
`.trim());

19
src/prompts/pm.ts Normal file
View File

@@ -0,0 +1,19 @@
import { registerPrompt } from './loader';
registerPrompt('pm', `
You are an autonomous Product Manager for a software project hosted on Gitea.
## Responsibilities
1. Create, update, and close Gitea issues.
2. Write and update docs in the repository.
3. Summarize project state and create reports.
4. Triage bugs and features by impact.
## When writing docs
- Clear and concise.
- Markdown formatting.
- Keep docs in sync with the codebase.
- Always commit after writing.
{{skills}}
`.trim());

View File

@@ -8,16 +8,9 @@ import { createJob, getJob, listJobs, updateJob } from './job-store';
import { runAgent } from './agent-runner'; import { runAgent } from './agent-runner';
import { AGENTS } from './agents'; import { AGENTS } from './agents';
import { ToolContext } from './tools'; import { ToolContext } from './tools';
import { PROTECTED_GITEA_REPOS } from './tools/security';
import { orchestratorChat, listSessions, clearSession } from './orchestrator'; import { orchestratorChat, listSessions, clearSession } from './orchestrator';
import { LLMMessage } from './llm';
// Protected Vibn platform repos — agents cannot clone or work in these workspaces
const PROTECTED_GITEA_REPOS = new Set([
'mark/vibn-frontend',
'mark/theia-code-os',
'mark/vibn-agent-runner',
'mark/vibn-api',
'mark/master-ai',
]);
const app = express(); const app = express();
app.use(cors()); app.use(cors());
@@ -186,14 +179,28 @@ app.get('/api/jobs/:id', (req: Request, res: Response) => {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
app.post('/orchestrator/chat', async (req: Request, res: Response) => { app.post('/orchestrator/chat', async (req: Request, res: Response) => {
const { message, session_id } = req.body as { message?: string; session_id?: string }; const {
message,
session_id,
history,
knowledge_context
} = req.body as {
message?: string;
session_id?: string;
history?: LLMMessage[];
knowledge_context?: string;
};
if (!message) { res.status(400).json({ error: '"message" is required' }); return; } if (!message) { res.status(400).json({ error: '"message" is required' }); return; }
const sessionId = session_id || `session_${Date.now()}`; const sessionId = session_id || `session_${Date.now()}`;
const ctx = buildContext(); const ctx = buildContext();
try { try {
const result = await orchestratorChat(sessionId, message, ctx); const result = await orchestratorChat(sessionId, message, ctx, {
preloadedHistory: history,
knowledgeContext: knowledge_context
});
res.json(result); res.json(result);
} catch (err) { } catch (err) {
res.status(500).json({ error: err instanceof Error ? err.message : String(err) }); res.status(500).json({ error: err instanceof Error ? err.message : String(err) });

View File

@@ -7,6 +7,7 @@ import './gitea';
import './coolify'; import './coolify';
import './agent'; import './agent';
import './memory'; import './memory';
import './skills';
// Re-export the public API — identical surface to the old tools.ts // Re-export the public API — identical surface to the old tools.ts
export { ALL_TOOLS, executeTool, ToolDefinition } from './registry'; export { ALL_TOOLS, executeTool, ToolDefinition } from './registry';

62
src/tools/skills.ts Normal file
View File

@@ -0,0 +1,62 @@
import { registerTool } from './registry';
import { ToolContext } from './context';
const SKILL_FILE = 'SKILL.md';
const SKILLS_DIR = '.skills';
async function giteaGetContents(repo: string, path: string, ctx: ToolContext): Promise<any> {
const res = await fetch(`${ctx.gitea.apiUrl}/api/v1/repos/${repo}/contents/${path}`, {
headers: { 'Authorization': `token ${ctx.gitea.apiToken}` }
});
if (!res.ok) return null;
return res.json();
}
registerTool({
name: 'list_skills',
description: `List available skills for a project repo. Skills are stored in .skills/<name>/SKILL.md and provide reusable instructions the agent should follow (e.g. deploy process, test commands, code conventions).`,
parameters: {
type: 'object',
properties: {
repo: { type: 'string', description: 'Repo in "owner/name" format' }
},
required: ['repo']
},
async handler(args, ctx) {
const repo = String(args.repo);
const contents = await giteaGetContents(repo, SKILLS_DIR, ctx);
if (!contents || !Array.isArray(contents)) {
return { skills: [], message: `No .skills/ directory found in ${repo}` };
}
const skills = contents
.filter((entry: any) => entry.type === 'dir')
.map((entry: any) => ({ name: entry.name, path: entry.path }));
return { repo, skills };
}
});
registerTool({
name: 'get_skill',
description: `Read the full content of a specific skill from a project repo. Call list_skills first to see what's available. Use this before spawning agents so they have the relevant project-specific instructions.`,
parameters: {
type: 'object',
properties: {
repo: { type: 'string', description: 'Repo in "owner/name" format' },
skill_name: { type: 'string', description: 'Skill name (directory name inside .skills/)' }
},
required: ['repo', 'skill_name']
},
async handler(args, ctx) {
const repo = String(args.repo);
const skillName = String(args.skill_name);
const filePath = `${SKILLS_DIR}/${skillName}/${SKILL_FILE}`;
const file = await giteaGetContents(repo, filePath, ctx);
if (!file || !file.content) {
return { error: `Skill "${skillName}" not found in ${repo}. Try list_skills to see available skills.` };
}
const content = Buffer.from(file.content, 'base64').toString('utf8');
// Strip YAML frontmatter if present, return just the markdown body
const body = content.replace(/^---[\s\S]*?---\s*/m, '').trim();
return { repo, skill: skillName, content: body };
}
});