chore: convert submodules to standard directories for true monorepo structure
This commit is contained in:
Submodule vibn-agent-runner deleted from 0d19f45cb8
46
vibn-agent-runner/Dockerfile
Normal file
46
vibn-agent-runner/Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
||||
FROM node:20-slim
|
||||
|
||||
# Install ripgrep, git, and docker CLI
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ripgrep \
|
||||
git \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
&& install -m 0755 -d /etc/apt/keyrings \
|
||||
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
|
||||
&& chmod a+r /etc/apt/keyrings/docker.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
||||
> /etc/apt/sources.list.d/docker.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends docker-ce-cli \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install all deps including devDeps (tsc needs them).
|
||||
# Override NODE_ENV so Coolify's build-time NODE_ENV=production doesn't skip devDeps.
|
||||
COPY package*.json ./
|
||||
RUN NODE_ENV=development npm ci
|
||||
|
||||
# Copy source and compile
|
||||
COPY tsconfig.json ./
|
||||
COPY src/ ./src/
|
||||
RUN npm run build
|
||||
|
||||
# Prune dev deps after build
|
||||
RUN npm prune --omit=dev
|
||||
|
||||
# Create workspace dir
|
||||
RUN mkdir -p /workspaces
|
||||
|
||||
# Git identity for commits made by agents
|
||||
RUN git config --global user.email "agent@vibnai.com" && \
|
||||
git config --global user.name "Vibn Agent Runner"
|
||||
|
||||
EXPOSE 3333
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3333
|
||||
|
||||
CMD ["node", "dist/server.js"]
|
||||
190
vibn-agent-runner/PROJECT.md
Normal file
190
vibn-agent-runner/PROJECT.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# vibn-agent-runner Project Documentation
|
||||
|
||||
## What it does
|
||||
|
||||
The `vibn-agent-runner` is a service responsible for executing tasks or "agents" within the Vibn ecosystem. It likely receives requests to run specific agents, manages their execution, and reports back their status or results. This service acts as a crucial component for extending Vibn's capabilities through custom or predefined agents.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### `GET /api/status`
|
||||
|
||||
Retrieves server status and job statistics.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **URL:** `/api/status`
|
||||
- **Response Body Example:**
|
||||
```json
|
||||
{
|
||||
"total_jobs": 100,
|
||||
"by_status": {
|
||||
"queued": 10,
|
||||
"running": 5,
|
||||
"completed": 80,
|
||||
"failed": 5
|
||||
},
|
||||
"uptime_seconds": 3600,
|
||||
"agents": ["Coder", "PM", "Marketing"]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /health`
|
||||
|
||||
Health check endpoint.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **URL:** `/health`
|
||||
- **Response Body Example:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"timestamp": "2023-10-27T10:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/agents`
|
||||
|
||||
Lists available agents.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **URL:** `/api/agents`
|
||||
- **Response Body Example:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Coder",
|
||||
"description": "An agent that writes and modifies code.",
|
||||
"tools": ["read_file", "write_file", "replace_in_file", "list_directory", "find_files", "search_code", "execute_command", "git_commit_and_push", "gitea_create_issue", "gitea_list_issues", "gitea_close_issue"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `POST /api/agent/run`
|
||||
|
||||
Submits a new job to run an agent.
|
||||
|
||||
- **Method:** `POST`
|
||||
- **URL:** `/api/agent/run`
|
||||
- **Request Body Example:**
|
||||
```json
|
||||
{
|
||||
"agent": "Coder",
|
||||
"task": "Fix bug in user authentication",
|
||||
"repo": "owner/repo-name"
|
||||
}
|
||||
```
|
||||
- **Response Body Example (Success):**
|
||||
```json
|
||||
{
|
||||
"jobId": "unique-job-id",
|
||||
"status": "queued"
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/jobs/:id`
|
||||
|
||||
Retrieves the status of a specific job.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **URL:** `/api/jobs/:id`
|
||||
- **Response Body Example:**
|
||||
```json
|
||||
{
|
||||
"id": "unique-job-id",
|
||||
"agent": "Coder",
|
||||
"task": "Fix bug in user authentication",
|
||||
"repo": "owner/repo-name",
|
||||
"status": "running",
|
||||
"progress": "Executing tests...",
|
||||
"createdAt": "2023-10-27T10:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/jobs`
|
||||
|
||||
Lists recent jobs.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **URL:** `/api/jobs`
|
||||
- **Query Parameters:**
|
||||
- `limit`: (Optional) Number of jobs to return (default: 20)
|
||||
- **Response Body Example:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "job-id-1",
|
||||
"agent": "Coder",
|
||||
"task": "Implement feature X",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"id": "job-id-2",
|
||||
"agent": "PM",
|
||||
"task": "Write project brief",
|
||||
"status": "running"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `POST /webhook/gitea`
|
||||
|
||||
Gitea webhook endpoint to trigger agents from issue events.
|
||||
|
||||
- **Method:** `POST`
|
||||
- **URL:** `/webhook/gitea`
|
||||
- **Headers:**
|
||||
- `X-Gitea-Event`: e.g., `issues`
|
||||
- `X-Gitea-Signature`: HMAC-SHA256 signature (if `WEBHOOK_SECRET` is set)
|
||||
- **Request Body:** Gitea webhook payload (JSON)
|
||||
- **Response Body Example:**
|
||||
```json
|
||||
{
|
||||
"jobId": "unique-job-id",
|
||||
"agent": "Coder",
|
||||
"event": "issues"
|
||||
}
|
||||
```
|
||||
|
||||
## How to run locally
|
||||
|
||||
To run the `vibn-agent-runner` locally, follow these steps:
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd vibn-agent-runner
|
||||
```
|
||||
|
||||
2. **Install dependencies:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
or
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
3. **Configure environment variables:**
|
||||
Copy the `.env.example` file to `.env` and update the values as needed.
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
4. **Build the project (if TypeScript):**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
or
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
5. **Start the application:**
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
or
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
The application should now be running, typically accessible at `http://localhost:3000` (or another port specified in your environment configuration).
|
||||
16
vibn-agent-runner/dist/agent-runner.d.ts
vendored
Normal file
16
vibn-agent-runner/dist/agent-runner.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import { AgentConfig } from './agents';
|
||||
import { ToolContext } from './tools';
|
||||
import { Job } from './job-store';
|
||||
export interface RunResult {
|
||||
finalText: string;
|
||||
toolCallCount: number;
|
||||
turns: number;
|
||||
model: string;
|
||||
}
|
||||
/**
|
||||
* Core agent execution loop — model-agnostic via the unified LLM client.
|
||||
*
|
||||
* Agents use their configured model tier (A/B/C) or a specific model ID.
|
||||
* Tool calling uses OpenAI format throughout.
|
||||
*/
|
||||
export declare function runAgent(job: Job, config: AgentConfig, task: string, ctx: ToolContext): Promise<RunResult>;
|
||||
82
vibn-agent-runner/dist/agent-runner.js
vendored
Normal file
82
vibn-agent-runner/dist/agent-runner.js
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.runAgent = runAgent;
|
||||
const llm_1 = require("./llm");
|
||||
const tools_1 = require("./tools");
|
||||
const loader_1 = require("./prompts/loader");
|
||||
const job_store_1 = require("./job-store");
|
||||
const MAX_TURNS = 40;
|
||||
/**
|
||||
* Core agent execution loop — model-agnostic via the unified LLM client.
|
||||
*
|
||||
* Agents use their configured model tier (A/B/C) or a specific model ID.
|
||||
* Tool calling uses OpenAI format throughout.
|
||||
*/
|
||||
async function runAgent(job, config, task, ctx) {
|
||||
const llm = (0, llm_1.createLLM)(config.model, { temperature: 0.2 });
|
||||
const oaiTools = (0, llm_1.toOAITools)(config.tools);
|
||||
const history = [
|
||||
{ role: 'user', content: task }
|
||||
];
|
||||
let toolCallCount = 0;
|
||||
let turn = 0;
|
||||
let finalText = '';
|
||||
(0, job_store_1.updateJob)(job.id, { status: 'running', progress: `Starting ${config.name} (${llm.modelId})…` });
|
||||
while (turn < MAX_TURNS) {
|
||||
turn++;
|
||||
const systemPrompt = (0, loader_1.resolvePrompt)(config.promptId);
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...history
|
||||
];
|
||||
const response = await llm.chat(messages, oaiTools, 8192);
|
||||
// Build assistant message for history
|
||||
const assistantMsg = {
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
tool_calls: response.tool_calls.length > 0 ? response.tool_calls : undefined
|
||||
};
|
||||
history.push(assistantMsg);
|
||||
// No tool calls — agent is done
|
||||
if (response.tool_calls.length === 0) {
|
||||
finalText = response.content ?? '';
|
||||
break;
|
||||
}
|
||||
// Execute tool calls
|
||||
for (const tc of response.tool_calls) {
|
||||
const fnName = tc.function.name;
|
||||
let fnArgs = {};
|
||||
try {
|
||||
fnArgs = JSON.parse(tc.function.arguments || '{}');
|
||||
}
|
||||
catch { /* bad JSON */ }
|
||||
toolCallCount++;
|
||||
(0, job_store_1.updateJob)(job.id, {
|
||||
progress: `Turn ${turn}: calling ${fnName}…`,
|
||||
toolCalls: [...(job.toolCalls || []), {
|
||||
turn,
|
||||
tool: fnName,
|
||||
args: fnArgs,
|
||||
timestamp: new Date().toISOString()
|
||||
}]
|
||||
});
|
||||
let result;
|
||||
try {
|
||||
result = await (0, tools_1.executeTool)(fnName, fnArgs, ctx);
|
||||
}
|
||||
catch (err) {
|
||||
result = { error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
history.push({
|
||||
role: 'tool',
|
||||
tool_call_id: tc.id,
|
||||
name: fnName,
|
||||
content: typeof result === 'string' ? result : JSON.stringify(result)
|
||||
});
|
||||
}
|
||||
}
|
||||
if (turn >= MAX_TURNS && !finalText) {
|
||||
finalText = `Agent hit the ${MAX_TURNS}-turn safety limit. Tool calls made: ${toolCallCount}.`;
|
||||
}
|
||||
return { finalText, toolCallCount, turns: turn, model: llm.modelId };
|
||||
}
|
||||
34
vibn-agent-runner/dist/agent-session-runner.d.ts
vendored
Normal file
34
vibn-agent-runner/dist/agent-session-runner.d.ts
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* agent-session-runner.ts
|
||||
*
|
||||
* Streaming variant of runAgent wired to a VIBN agent_sessions row.
|
||||
* After every LLM turn + tool call, it PATCHes the session in the VIBN DB
|
||||
* so the frontend can poll (and later WebSocket) the live output.
|
||||
*
|
||||
* Key differences from runAgent:
|
||||
* - Accepts an `emit` callback instead of updating job-store
|
||||
* - Accepts an `isStopped` check so the frontend can cancel mid-run
|
||||
* - Tracks which files were written/modified for the changed_files panel
|
||||
* - Calls vibn-frontend's PATCH /api/projects/[id]/agent/sessions/[sid]
|
||||
*/
|
||||
import { AgentConfig } from './agents';
|
||||
import { ToolContext } from './tools';
|
||||
export interface OutputLine {
|
||||
ts: string;
|
||||
type: 'step' | 'stdout' | 'stderr' | 'info' | 'error' | 'done';
|
||||
text: string;
|
||||
}
|
||||
export interface SessionRunOptions {
|
||||
sessionId: string;
|
||||
projectId: string;
|
||||
vibnApiUrl: string;
|
||||
appPath: string;
|
||||
repoRoot?: string;
|
||||
isStopped: () => boolean;
|
||||
autoApprove?: boolean;
|
||||
giteaRepo?: string;
|
||||
coolifyAppUuid?: string;
|
||||
coolifyApiUrl?: string;
|
||||
coolifyApiToken?: string;
|
||||
}
|
||||
export declare function runSessionAgent(config: AgentConfig, task: string, ctx: ToolContext, opts: SessionRunOptions): Promise<void>;
|
||||
262
vibn-agent-runner/dist/agent-session-runner.js
vendored
Normal file
262
vibn-agent-runner/dist/agent-session-runner.js
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
"use strict";
|
||||
/**
|
||||
* agent-session-runner.ts
|
||||
*
|
||||
* Streaming variant of runAgent wired to a VIBN agent_sessions row.
|
||||
* After every LLM turn + tool call, it PATCHes the session in the VIBN DB
|
||||
* so the frontend can poll (and later WebSocket) the live output.
|
||||
*
|
||||
* Key differences from runAgent:
|
||||
* - Accepts an `emit` callback instead of updating job-store
|
||||
* - Accepts an `isStopped` check so the frontend can cancel mid-run
|
||||
* - Tracks which files were written/modified for the changed_files panel
|
||||
* - Calls vibn-frontend's PATCH /api/projects/[id]/agent/sessions/[sid]
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.runSessionAgent = runSessionAgent;
|
||||
const child_process_1 = require("child_process");
|
||||
const llm_1 = require("./llm");
|
||||
const tools_1 = require("./tools");
|
||||
const loader_1 = require("./prompts/loader");
|
||||
const vibn_events_ingest_1 = require("./vibn-events-ingest");
|
||||
const MAX_TURNS = 60;
|
||||
// ── VIBN DB bridge ────────────────────────────────────────────────────────────
|
||||
async function patchSession(opts, payload) {
|
||||
const url = `${opts.vibnApiUrl}/api/projects/${opts.projectId}/agent/sessions/${opts.sessionId}`;
|
||||
try {
|
||||
await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json', 'x-agent-runner-secret': process.env.AGENT_RUNNER_SECRET ?? '' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
// Log but don't crash — output will be lost for this line but loop continues
|
||||
console.warn('[session-runner] PATCH failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
}
|
||||
function now() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
// ── File change tracking ──────────────────────────────────────────────────────
|
||||
const FILE_WRITE_TOOLS = new Set(['write_file', 'replace_in_file', 'create_file']);
|
||||
function extractChangedFile(toolName, args, workspaceRoot, appPath) {
|
||||
if (!FILE_WRITE_TOOLS.has(toolName))
|
||||
return null;
|
||||
const rawPath = String(args.path ?? args.file_path ?? '');
|
||||
if (!rawPath)
|
||||
return null;
|
||||
// Make path relative to appPath for display
|
||||
const fullPrefix = `${workspaceRoot}/${appPath}/`;
|
||||
const appPrefix = `${appPath}/`;
|
||||
let displayPath = rawPath
|
||||
.replace(fullPrefix, '')
|
||||
.replace(appPrefix, '');
|
||||
const fileStatus = toolName === 'write_file' ? 'added' : 'modified';
|
||||
return { path: displayPath, status: fileStatus };
|
||||
}
|
||||
// ── Auto-commit helper ────────────────────────────────────────────────────────
|
||||
async function autoCommitAndDeploy(opts, task, emit) {
|
||||
const repoRoot = opts.repoRoot;
|
||||
if (!repoRoot || !opts.giteaRepo) {
|
||||
await emit({ ts: now(), type: 'info', text: 'Auto-approve skipped — no repo root available.' });
|
||||
return;
|
||||
}
|
||||
const gitOpts = { cwd: repoRoot, stdio: 'pipe' };
|
||||
const giteaApiUrl = process.env.GITEA_API_URL || '';
|
||||
const giteaUsername = process.env.GITEA_USERNAME || 'agent';
|
||||
const giteaToken = process.env.GITEA_API_TOKEN || '';
|
||||
try {
|
||||
try {
|
||||
(0, child_process_1.execSync)('git config user.email "agent@vibnai.com"', gitOpts);
|
||||
(0, child_process_1.execSync)('git config user.name "VIBN Agent"', gitOpts);
|
||||
}
|
||||
catch { /* already set */ }
|
||||
(0, child_process_1.execSync)('git add -A', gitOpts);
|
||||
const status = (0, child_process_1.execSync)('git status --porcelain', gitOpts).toString().trim();
|
||||
if (!status) {
|
||||
await emit({ ts: now(), type: 'info', text: '✓ No file changes to commit.' });
|
||||
await patchSession(opts, { status: 'approved' });
|
||||
return;
|
||||
}
|
||||
const commitMsg = `agent: ${task.slice(0, 72)}`;
|
||||
(0, child_process_1.execSync)(`git commit -m ${JSON.stringify(commitMsg)}`, gitOpts);
|
||||
await emit({ ts: now(), type: 'info', text: `✓ Committed: "${commitMsg}"` });
|
||||
const authedUrl = `${giteaApiUrl}/${opts.giteaRepo}.git`
|
||||
.replace('https://', `https://${giteaUsername}:${giteaToken}@`);
|
||||
(0, child_process_1.execSync)(`git push "${authedUrl}" HEAD:main`, gitOpts);
|
||||
await emit({ ts: now(), type: 'info', text: '✓ Pushed to Gitea.' });
|
||||
// Optional Coolify deploy
|
||||
let deployed = false;
|
||||
if (opts.coolifyApiUrl && opts.coolifyApiToken && opts.coolifyAppUuid) {
|
||||
try {
|
||||
const deployRes = await fetch(`${opts.coolifyApiUrl}/api/v1/applications/${opts.coolifyAppUuid}/start`, { method: 'POST', headers: { Authorization: `Bearer ${opts.coolifyApiToken}` } });
|
||||
deployed = deployRes.ok;
|
||||
if (deployed)
|
||||
await emit({ ts: now(), type: 'info', text: '✓ Deployment triggered.' });
|
||||
}
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
await patchSession(opts, {
|
||||
status: 'approved',
|
||||
outputLine: {
|
||||
ts: now(), type: 'done',
|
||||
text: `✓ Auto-committed & ${deployed ? 'deployed' : 'pushed'}. No approval needed.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
await emit({ ts: now(), type: 'error', text: `Auto-commit failed: ${msg}` });
|
||||
// Fall back to done so user can manually approve
|
||||
await patchSession(opts, { status: 'done' });
|
||||
}
|
||||
}
|
||||
// ── Main streaming execution loop ─────────────────────────────────────────────
|
||||
async function runSessionAgent(config, task, ctx, opts) {
|
||||
const llm = (0, llm_1.createLLM)(config.model, { temperature: 0.2 });
|
||||
const oaiTools = (0, llm_1.toOAITools)(config.tools);
|
||||
const emit = async (line) => {
|
||||
console.log(`[session ${opts.sessionId}] ${line.type}: ${line.text}`);
|
||||
await Promise.all([
|
||||
patchSession(opts, { outputLine: line }),
|
||||
(0, vibn_events_ingest_1.ingestSessionEvents)(opts.vibnApiUrl, opts.projectId, opts.sessionId, [
|
||||
{
|
||||
type: `output.${line.type}`,
|
||||
payload: { text: line.text },
|
||||
ts: line.ts,
|
||||
},
|
||||
]),
|
||||
]);
|
||||
};
|
||||
await emit({ ts: now(), type: 'info', text: `Agent starting (${llm.modelId}) — working in ${opts.appPath}` });
|
||||
// Scope the system prompt to the specific app within the monorepo
|
||||
const basePrompt = (0, loader_1.resolvePrompt)(config.promptId);
|
||||
const scopedPrompt = `${basePrompt}
|
||||
|
||||
## Active context
|
||||
You are working inside the monorepo directory: ${opts.appPath}
|
||||
All file paths you use should be relative to this directory unless otherwise specified.
|
||||
When running commands, always cd into ${opts.appPath} first unless already there.
|
||||
Do NOT run git commit or git push — the platform handles committing after you finish.
|
||||
`;
|
||||
const history = [
|
||||
{ role: 'user', content: task }
|
||||
];
|
||||
let turn = 0;
|
||||
let finalText = '';
|
||||
const trackedFiles = new Map(); // path → status
|
||||
while (turn < MAX_TURNS) {
|
||||
// Check for stop signal between turns
|
||||
if (opts.isStopped()) {
|
||||
await emit({ ts: now(), type: 'info', text: 'Stopped by user.' });
|
||||
await patchSession(opts, { status: 'stopped' });
|
||||
return;
|
||||
}
|
||||
turn++;
|
||||
await emit({ ts: now(), type: 'info', text: `Turn ${turn} — thinking…` });
|
||||
const messages = [
|
||||
{ role: 'system', content: scopedPrompt },
|
||||
...history
|
||||
];
|
||||
let response;
|
||||
try {
|
||||
response = await llm.chat(messages, oaiTools, 8192);
|
||||
}
|
||||
catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
await emit({ ts: now(), type: 'error', text: `LLM error: ${msg}` });
|
||||
await patchSession(opts, { status: 'failed', error: msg });
|
||||
return;
|
||||
}
|
||||
const assistantMsg = {
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
tool_calls: response.tool_calls.length > 0 ? response.tool_calls : undefined
|
||||
};
|
||||
history.push(assistantMsg);
|
||||
// Agent finished — no more tool calls
|
||||
if (response.tool_calls.length === 0) {
|
||||
finalText = response.content ?? 'Task complete.';
|
||||
break;
|
||||
}
|
||||
// Execute each tool call
|
||||
for (const tc of response.tool_calls) {
|
||||
if (opts.isStopped())
|
||||
break;
|
||||
const fnName = tc.function.name;
|
||||
let fnArgs = {};
|
||||
try {
|
||||
fnArgs = JSON.parse(tc.function.arguments || '{}');
|
||||
}
|
||||
catch { /* bad JSON */ }
|
||||
// Human-readable step label
|
||||
const stepLabel = buildStepLabel(fnName, fnArgs);
|
||||
await emit({ ts: now(), type: 'step', text: stepLabel });
|
||||
let result;
|
||||
try {
|
||||
result = await (0, tools_1.executeTool)(fnName, fnArgs, ctx);
|
||||
}
|
||||
catch (err) {
|
||||
result = { error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
// Stream stdout/stderr if present
|
||||
if (result && typeof result === 'object') {
|
||||
const r = result;
|
||||
if (r.stdout && String(r.stdout).trim()) {
|
||||
for (const line of String(r.stdout).split('\n').filter(Boolean).slice(0, 40)) {
|
||||
await emit({ ts: now(), type: 'stdout', text: line });
|
||||
}
|
||||
}
|
||||
if (r.stderr && String(r.stderr).trim()) {
|
||||
for (const line of String(r.stderr).split('\n').filter(Boolean).slice(0, 20)) {
|
||||
await emit({ ts: now(), type: 'stderr', text: line });
|
||||
}
|
||||
}
|
||||
if (r.error) {
|
||||
await emit({ ts: now(), type: 'error', text: String(r.error) });
|
||||
}
|
||||
}
|
||||
// Track file changes
|
||||
const changed = extractChangedFile(fnName, fnArgs, ctx.workspaceRoot, opts.appPath);
|
||||
if (changed && !trackedFiles.has(changed.path)) {
|
||||
trackedFiles.set(changed.path, changed.status);
|
||||
await patchSession(opts, { changedFile: changed });
|
||||
await emit({ ts: now(), type: 'info', text: `${changed.status === 'added' ? '+ Created' : '~ Modified'} ${changed.path}` });
|
||||
}
|
||||
history.push({
|
||||
role: 'tool',
|
||||
tool_call_id: tc.id,
|
||||
name: fnName,
|
||||
content: typeof result === 'string' ? result : JSON.stringify(result)
|
||||
});
|
||||
}
|
||||
}
|
||||
if (turn >= MAX_TURNS && !finalText) {
|
||||
finalText = `Hit the ${MAX_TURNS}-turn limit. Stopping.`;
|
||||
}
|
||||
await emit({ ts: now(), type: 'done', text: finalText });
|
||||
if (opts.autoApprove) {
|
||||
await autoCommitAndDeploy(opts, task, emit);
|
||||
}
|
||||
else {
|
||||
await patchSession(opts, {
|
||||
status: 'done',
|
||||
outputLine: { ts: now(), type: 'done', text: '✓ Complete — review changes and approve to commit.' },
|
||||
});
|
||||
}
|
||||
}
|
||||
// ── Step label helpers ────────────────────────────────────────────────────────
|
||||
function buildStepLabel(tool, args) {
|
||||
switch (tool) {
|
||||
case 'read_file': return `Read ${args.path ?? args.file_path}`;
|
||||
case 'write_file': return `Write ${args.path ?? args.file_path}`;
|
||||
case 'replace_in_file': return `Edit ${args.path ?? args.file_path}`;
|
||||
case 'list_directory': return `List ${args.path ?? '.'}`;
|
||||
case 'find_files': return `Find files: ${args.pattern}`;
|
||||
case 'search_code': return `Search: ${args.query}`;
|
||||
case 'execute_command': return `Run: ${String(args.command ?? '').slice(0, 80)}`;
|
||||
case 'git_commit_and_push': return `Git commit: "${args.message}"`;
|
||||
default: return `${tool}(${JSON.stringify(args).slice(0, 60)})`;
|
||||
}
|
||||
}
|
||||
1
vibn-agent-runner/dist/agents/atlas.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/agents/atlas.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
10
vibn-agent-runner/dist/agents/atlas.js
vendored
Normal file
10
vibn-agent-runner/dist/agents/atlas.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const registry_1 = require("./registry");
|
||||
(0, registry_1.registerAgent)({
|
||||
name: 'Atlas',
|
||||
description: 'PRD agent — guides users through structured product discovery and produces a comprehensive requirements document',
|
||||
model: 'A', // Gemini Flash — fast, conversational, cost-effective for dialogue
|
||||
promptId: 'atlas',
|
||||
tools: (0, registry_1.pick)(['web_search', 'finalize_prd'])
|
||||
});
|
||||
1
vibn-agent-runner/dist/agents/coder.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/agents/coder.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
16
vibn-agent-runner/dist/agents/coder.js
vendored
Normal file
16
vibn-agent-runner/dist/agents/coder.js
vendored
Normal 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'
|
||||
])
|
||||
});
|
||||
1
vibn-agent-runner/dist/agents/import-analyzer.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/agents/import-analyzer.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
13
vibn-agent-runner/dist/agents/import-analyzer.js
vendored
Normal file
13
vibn-agent-runner/dist/agents/import-analyzer.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const registry_1 = require("./registry");
|
||||
(0, registry_1.registerAgent)({
|
||||
name: 'ImportAnalyzer',
|
||||
description: 'Reads an imported codebase end-to-end and produces CODEBASE_MAP.md and MIGRATION_PLAN.md',
|
||||
model: 'B',
|
||||
promptId: 'import-analyzer',
|
||||
tools: (0, registry_1.pick)([
|
||||
'read_file', 'write_file', 'list_directory', 'find_files', 'search_code',
|
||||
'git_commit_and_push',
|
||||
])
|
||||
});
|
||||
13
vibn-agent-runner/dist/agents/index.d.ts
vendored
Normal file
13
vibn-agent-runner/dist/agents/index.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import '../prompts/orchestrator';
|
||||
import '../prompts/coder';
|
||||
import '../prompts/pm';
|
||||
import '../prompts/marketing';
|
||||
import '../prompts/atlas';
|
||||
import '../prompts/import-analyzer';
|
||||
import './orchestrator';
|
||||
import './coder';
|
||||
import './pm';
|
||||
import './marketing';
|
||||
import './atlas';
|
||||
import './import-analyzer';
|
||||
export { AgentConfig, AGENTS, getAgent, allAgents, pick } from './registry';
|
||||
23
vibn-agent-runner/dist/agents/index.js
vendored
Normal file
23
vibn-agent-runner/dist/agents/index.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
"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");
|
||||
require("../prompts/atlas");
|
||||
require("../prompts/import-analyzer");
|
||||
// Import agent files — side effects register each agent into the registry
|
||||
require("./orchestrator");
|
||||
require("./coder");
|
||||
require("./pm");
|
||||
require("./marketing");
|
||||
require("./atlas");
|
||||
require("./import-analyzer");
|
||||
// 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
vibn-agent-runner/dist/agents/marketing.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/agents/marketing.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
14
vibn-agent-runner/dist/agents/marketing.js
vendored
Normal file
14
vibn-agent-runner/dist/agents/marketing.js
vendored
Normal 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
vibn-agent-runner/dist/agents/orchestrator.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/agents/orchestrator.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
17
vibn-agent-runner/dist/agents/orchestrator.js
vendored
Normal file
17
vibn-agent-runner/dist/agents/orchestrator.js
vendored
Normal 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
vibn-agent-runner/dist/agents/pm.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/agents/pm.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
15
vibn-agent-runner/dist/agents/pm.js
vendored
Normal file
15
vibn-agent-runner/dist/agents/pm.js
vendored
Normal 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
vibn-agent-runner/dist/agents/registry.d.ts
vendored
Normal file
18
vibn-agent-runner/dist/agents/registry.d.ts
vendored
Normal 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
vibn-agent-runner/dist/agents/registry.js
vendored
Normal file
34
vibn-agent-runner/dist/agents/registry.js
vendored
Normal 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));
|
||||
}
|
||||
23
vibn-agent-runner/dist/atlas.d.ts
vendored
Normal file
23
vibn-agent-runner/dist/atlas.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import { LLMMessage } from './llm';
|
||||
import { ToolContext } from './tools';
|
||||
export declare function clearAtlasSession(sessionId: string): void;
|
||||
export declare function listAtlasSessions(): {
|
||||
id: string;
|
||||
messages: number;
|
||||
prdReady: boolean;
|
||||
createdAt: string;
|
||||
lastActiveAt: string;
|
||||
}[];
|
||||
export interface AtlasChatResult {
|
||||
reply: string;
|
||||
sessionId: string;
|
||||
history: LLMMessage[];
|
||||
/** Set when Atlas has called finalize_prd — contains the full PRD markdown */
|
||||
prdContent: string | null;
|
||||
model: string;
|
||||
}
|
||||
export declare function atlasChat(sessionId: string, userMessage: string, ctx: ToolContext, opts?: {
|
||||
preloadedHistory?: LLMMessage[];
|
||||
/** When true, the user message is an internal init trigger and should not be stored in history */
|
||||
isInit?: boolean;
|
||||
}): Promise<AtlasChatResult>;
|
||||
126
vibn-agent-runner/dist/atlas.js
vendored
Normal file
126
vibn-agent-runner/dist/atlas.js
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.clearAtlasSession = clearAtlasSession;
|
||||
exports.listAtlasSessions = listAtlasSessions;
|
||||
exports.atlasChat = atlasChat;
|
||||
const llm_1 = require("./llm");
|
||||
const tools_1 = require("./tools");
|
||||
const loader_1 = require("./prompts/loader");
|
||||
const prd_1 = require("./tools/prd");
|
||||
const MAX_TURNS = 10; // Atlas is conversational — low turn count, no deep tool loops
|
||||
const sessions = new Map();
|
||||
function getOrCreateSession(sessionId) {
|
||||
if (!sessions.has(sessionId)) {
|
||||
sessions.set(sessionId, {
|
||||
id: sessionId,
|
||||
history: [],
|
||||
prdContent: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastActiveAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
const session = sessions.get(sessionId);
|
||||
session.lastActiveAt = new Date().toISOString();
|
||||
return session;
|
||||
}
|
||||
function clearAtlasSession(sessionId) {
|
||||
sessions.delete(sessionId);
|
||||
}
|
||||
function listAtlasSessions() {
|
||||
return Array.from(sessions.values()).map(s => ({
|
||||
id: s.id,
|
||||
messages: s.history.length,
|
||||
prdReady: s.prdContent !== null,
|
||||
createdAt: s.createdAt,
|
||||
lastActiveAt: s.lastActiveAt
|
||||
}));
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main chat handler
|
||||
// ---------------------------------------------------------------------------
|
||||
const ATLAS_TOOLS = tools_1.ALL_TOOLS.filter(t => ['finalize_prd', 'web_search'].includes(t.name));
|
||||
async function atlasChat(sessionId, userMessage, ctx, opts) {
|
||||
const llm = (0, llm_1.createLLM)(process.env.ATLAS_MODEL ?? 'A', { temperature: 0.5 });
|
||||
const session = getOrCreateSession(sessionId);
|
||||
// Seed from DB history if this is a fresh in-memory session
|
||||
if (opts?.preloadedHistory && opts.preloadedHistory.length > 0 && session.history.length === 0) {
|
||||
session.history = [...opts.preloadedHistory];
|
||||
}
|
||||
const oaiTools = (0, llm_1.toOAITools)(ATLAS_TOOLS);
|
||||
const systemPrompt = (0, loader_1.resolvePrompt)('atlas');
|
||||
// Always push the user message so Gemini gets a valid conversation (requires at least one user turn).
|
||||
// For init triggers, we mark it so we can strip it from the returned history — it's an internal
|
||||
// prompt, not a real user message, and shouldn't appear in the conversation UI or DB.
|
||||
const INIT_MARKER = '__atlas_init_marker__';
|
||||
session.history.push({
|
||||
role: 'user',
|
||||
content: opts?.isInit ? INIT_MARKER + userMessage : userMessage
|
||||
});
|
||||
const buildMessages = () => [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...session.history.slice(-60).map(m =>
|
||||
// Strip the init marker before sending to the LLM
|
||||
m.role === 'user' && typeof m.content === 'string' && m.content.startsWith(INIT_MARKER)
|
||||
? { ...m, content: m.content.slice(INIT_MARKER.length) }
|
||||
: m)
|
||||
];
|
||||
let turn = 0;
|
||||
let finalReply = '';
|
||||
let prdContent = session.prdContent;
|
||||
while (turn < MAX_TURNS) {
|
||||
turn++;
|
||||
const response = await llm.chat(buildMessages(), oaiTools, 4096);
|
||||
const hasContent = response.content !== null && response.content !== '';
|
||||
const hasToolCalls = response.tool_calls.length > 0;
|
||||
if (hasContent || hasToolCalls) {
|
||||
session.history.push({
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
tool_calls: hasToolCalls ? response.tool_calls : undefined
|
||||
});
|
||||
}
|
||||
if (!hasToolCalls) {
|
||||
finalReply = response.content ?? '';
|
||||
break;
|
||||
}
|
||||
// Execute tool calls (only finalize_prd for Atlas)
|
||||
for (const tc of response.tool_calls) {
|
||||
let fnArgs = {};
|
||||
try {
|
||||
fnArgs = JSON.parse(tc.function.arguments || '{}');
|
||||
}
|
||||
catch { /* bad JSON */ }
|
||||
let result;
|
||||
try {
|
||||
result = await (0, tools_1.executeTool)(tc.function.name, fnArgs, ctx);
|
||||
}
|
||||
catch (err) {
|
||||
result = { error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
// Check if PRD was just saved
|
||||
const stored = prd_1.prdStore.get(ctx.workspaceRoot);
|
||||
if (stored && !prdContent) {
|
||||
prdContent = stored;
|
||||
session.prdContent = stored;
|
||||
prd_1.prdStore.delete(ctx.workspaceRoot); // consume it
|
||||
}
|
||||
session.history.push({
|
||||
role: 'tool',
|
||||
tool_call_id: tc.id,
|
||||
name: tc.function.name,
|
||||
content: typeof result === 'string' ? result : JSON.stringify(result)
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
reply: finalReply,
|
||||
sessionId,
|
||||
history: session.history
|
||||
// Drop the internal init user turn — it's not a real user message
|
||||
.filter(m => !(m.role === 'user' && typeof m.content === 'string' && m.content.startsWith(INIT_MARKER)))
|
||||
.filter(m => m.role !== 'assistant' || m.content || m.tool_calls?.length)
|
||||
.slice(-60),
|
||||
prdContent,
|
||||
model: llm.modelId
|
||||
};
|
||||
}
|
||||
24
vibn-agent-runner/dist/job-store.d.ts
vendored
Normal file
24
vibn-agent-runner/dist/job-store.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
export type JobStatus = 'queued' | 'running' | 'completed' | 'failed';
|
||||
export interface ToolCallRecord {
|
||||
turn: number;
|
||||
tool: string;
|
||||
args: unknown;
|
||||
timestamp: string;
|
||||
}
|
||||
export interface Job {
|
||||
id: string;
|
||||
agent: string;
|
||||
task: string;
|
||||
repo?: string;
|
||||
status: JobStatus;
|
||||
progress: string;
|
||||
toolCalls: ToolCallRecord[];
|
||||
result?: string;
|
||||
error?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
export declare function createJob(agent: string, task: string, repo?: string): Job;
|
||||
export declare function getJob(id: string): Job | undefined;
|
||||
export declare function updateJob(id: string, updates: Partial<Job>): Job | undefined;
|
||||
export declare function listJobs(limit?: number): Job[];
|
||||
43
vibn-agent-runner/dist/job-store.js
vendored
Normal file
43
vibn-agent-runner/dist/job-store.js
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createJob = createJob;
|
||||
exports.getJob = getJob;
|
||||
exports.updateJob = updateJob;
|
||||
exports.listJobs = listJobs;
|
||||
const uuid_1 = require("uuid");
|
||||
// ---------------------------------------------------------------------------
|
||||
// In-memory store (swap for Redis/DB if scaling horizontally)
|
||||
// ---------------------------------------------------------------------------
|
||||
const store = new Map();
|
||||
function createJob(agent, task, repo) {
|
||||
const job = {
|
||||
id: (0, uuid_1.v4)(),
|
||||
agent,
|
||||
task,
|
||||
repo,
|
||||
status: 'queued',
|
||||
progress: 'Job queued',
|
||||
toolCalls: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
store.set(job.id, job);
|
||||
return job;
|
||||
}
|
||||
function getJob(id) {
|
||||
return store.get(id);
|
||||
}
|
||||
function updateJob(id, updates) {
|
||||
const job = store.get(id);
|
||||
if (!job)
|
||||
return undefined;
|
||||
const updated = { ...job, ...updates, id, updatedAt: new Date().toISOString() };
|
||||
store.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
function listJobs(limit = 50) {
|
||||
const all = Array.from(store.values());
|
||||
return all
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.slice(0, limit);
|
||||
}
|
||||
78
vibn-agent-runner/dist/llm.d.ts
vendored
Normal file
78
vibn-agent-runner/dist/llm.d.ts
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
export interface LLMMessage {
|
||||
role: 'system' | 'user' | 'assistant' | 'tool';
|
||||
content: string | null;
|
||||
tool_calls?: LLMToolCall[];
|
||||
tool_call_id?: string;
|
||||
name?: string;
|
||||
}
|
||||
export interface LLMToolCall {
|
||||
id: string;
|
||||
type: 'function';
|
||||
function: {
|
||||
name: string;
|
||||
arguments: string;
|
||||
};
|
||||
}
|
||||
export interface LLMTool {
|
||||
type: 'function';
|
||||
function: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
export interface LLMResponse {
|
||||
content: string | null;
|
||||
reasoning: string | null;
|
||||
tool_calls: LLMToolCall[];
|
||||
finish_reason: string;
|
||||
usage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
}
|
||||
export interface LLMClient {
|
||||
modelId: string;
|
||||
chat(messages: LLMMessage[], tools?: LLMTool[], maxTokens?: number): Promise<LLMResponse>;
|
||||
}
|
||||
export declare class VertexOpenAIClient implements LLMClient {
|
||||
modelId: string;
|
||||
private projectId;
|
||||
private region;
|
||||
private temperature;
|
||||
constructor(modelId: string, opts?: {
|
||||
projectId?: string;
|
||||
region?: string;
|
||||
temperature?: number;
|
||||
});
|
||||
chat(messages: LLMMessage[], tools?: LLMTool[], maxTokens?: number): Promise<LLMResponse>;
|
||||
}
|
||||
export declare class GeminiClient implements LLMClient {
|
||||
modelId: string;
|
||||
private temperature;
|
||||
constructor(modelId?: string, opts?: {
|
||||
temperature?: number;
|
||||
});
|
||||
chat(messages: LLMMessage[], tools?: LLMTool[], maxTokens?: number): Promise<LLMResponse>;
|
||||
}
|
||||
export declare class AnthropicVertexClient implements LLMClient {
|
||||
modelId: string;
|
||||
private projectId;
|
||||
private region;
|
||||
constructor(modelId: string, opts?: {
|
||||
projectId?: string;
|
||||
region?: string;
|
||||
});
|
||||
private buildClient;
|
||||
chat(messages: LLMMessage[], tools?: LLMTool[], maxTokens?: number): Promise<LLMResponse>;
|
||||
}
|
||||
export type ModelTier = 'A' | 'B' | 'C';
|
||||
export declare function createLLM(modelOrTier: string | ModelTier, opts?: {
|
||||
temperature?: number;
|
||||
}): LLMClient;
|
||||
export declare function toOAITools(tools: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, unknown>;
|
||||
}>): LLMTool[];
|
||||
352
vibn-agent-runner/dist/llm.js
vendored
Normal file
352
vibn-agent-runner/dist/llm.js
vendored
Normal file
@@ -0,0 +1,352 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AnthropicVertexClient = exports.GeminiClient = exports.VertexOpenAIClient = void 0;
|
||||
exports.createLLM = createLLM;
|
||||
exports.toOAITools = toOAITools;
|
||||
const google_auth_library_1 = require("google-auth-library");
|
||||
const genai_1 = require("@google/genai");
|
||||
const vertex_sdk_1 = __importDefault(require("@anthropic-ai/vertex-sdk"));
|
||||
const uuid_1 = require("uuid");
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vertex AI OpenAI-compatible client
|
||||
// Used for: zai-org/glm-5-maas, anthropic/claude-sonnet-4-6, etc.
|
||||
// ---------------------------------------------------------------------------
|
||||
let _cachedToken = '';
|
||||
let _tokenExpiry = 0;
|
||||
// Build GoogleAuth with explicit service account credentials when available.
|
||||
// GCP_SA_KEY_BASE64: base64-encoded service account JSON key — safe to pass as
|
||||
// an env var since it contains no newlines or special shell characters.
|
||||
// Falls back to the GCP metadata server (works on VMs with correct scopes).
|
||||
function buildGoogleAuth() {
|
||||
const b64Key = process.env.GCP_SA_KEY_BASE64;
|
||||
if (b64Key) {
|
||||
try {
|
||||
const jsonStr = Buffer.from(b64Key, 'base64').toString('utf8');
|
||||
const credentials = JSON.parse(jsonStr);
|
||||
return new google_auth_library_1.GoogleAuth({ credentials, scopes: ['https://www.googleapis.com/auth/cloud-platform'] });
|
||||
}
|
||||
catch {
|
||||
console.warn('[llm] GCP_SA_KEY_BASE64 is set but failed to decode/parse — falling back to metadata server');
|
||||
}
|
||||
}
|
||||
return new google_auth_library_1.GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'] });
|
||||
}
|
||||
const _googleAuth = buildGoogleAuth();
|
||||
async function getVertexToken() {
|
||||
const now = Date.now();
|
||||
if (_cachedToken && now < _tokenExpiry)
|
||||
return _cachedToken;
|
||||
const client = await _googleAuth.getClient();
|
||||
const tokenResponse = await client.getAccessToken();
|
||||
_cachedToken = tokenResponse.token;
|
||||
_tokenExpiry = now + 55 * 60 * 1000; // tokens last 1hr, refresh at 55min
|
||||
return _cachedToken;
|
||||
}
|
||||
class VertexOpenAIClient {
|
||||
constructor(modelId, opts) {
|
||||
this.modelId = modelId;
|
||||
this.projectId = opts?.projectId ?? process.env.GCP_PROJECT_ID ?? 'master-ai-484822';
|
||||
this.region = opts?.region ?? 'global';
|
||||
this.temperature = opts?.temperature ?? 0.3;
|
||||
}
|
||||
async chat(messages, tools, maxTokens = 4096) {
|
||||
const base = this.region === 'global'
|
||||
? 'https://aiplatform.googleapis.com'
|
||||
: `https://${this.region}-aiplatform.googleapis.com`;
|
||||
const url = `${base}/v1/projects/${this.projectId}/locations/${this.region}/endpoints/openapi/chat/completions`;
|
||||
const body = {
|
||||
model: this.modelId,
|
||||
messages,
|
||||
max_tokens: maxTokens,
|
||||
temperature: this.temperature,
|
||||
stream: false
|
||||
};
|
||||
if (tools && tools.length > 0) {
|
||||
body.tools = tools;
|
||||
body.tool_choice = 'auto';
|
||||
}
|
||||
// Retry with exponential backoff on 429 / 503 (rate limit / overload)
|
||||
const MAX_RETRIES = 4;
|
||||
const RETRY_STATUSES = new Set([429, 503]);
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
const token = await getVertexToken();
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const choice = data.choices?.[0];
|
||||
const message = choice?.message ?? {};
|
||||
return {
|
||||
content: message.content ?? null,
|
||||
reasoning: message.reasoning_content ?? null,
|
||||
tool_calls: message.tool_calls ?? [],
|
||||
finish_reason: choice?.finish_reason ?? 'stop',
|
||||
usage: data.usage
|
||||
};
|
||||
}
|
||||
const errText = await res.text();
|
||||
// Force token refresh on 401
|
||||
if (res.status === 401)
|
||||
_tokenExpiry = 0;
|
||||
if (RETRY_STATUSES.has(res.status) && attempt < MAX_RETRIES) {
|
||||
// Check for Retry-After header, otherwise use exponential backoff
|
||||
const retryAfter = res.headers.get('retry-after');
|
||||
const waitMs = retryAfter
|
||||
? Math.min(parseInt(retryAfter, 10) * 1000, 60000)
|
||||
: Math.min(2 ** attempt * 2000 + Math.random() * 500, 30000);
|
||||
console.warn(`[llm] Vertex ${res.status} on attempt ${attempt + 1}/${MAX_RETRIES + 1} — retrying in ${Math.round(waitMs / 1000)}s`);
|
||||
await new Promise(r => setTimeout(r, waitMs));
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Vertex API ${res.status}: ${errText.slice(0, 400)}`);
|
||||
}
|
||||
// TypeScript requires an explicit throw after the loop (unreachable in practice)
|
||||
throw new Error('Vertex API: exceeded max retries');
|
||||
}
|
||||
}
|
||||
exports.VertexOpenAIClient = VertexOpenAIClient;
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gemini client via @google/genai SDK
|
||||
// Used for: Tier A (fast/cheap routing, summaries, log parsing)
|
||||
// Converts to/from OpenAI message format internally.
|
||||
// ---------------------------------------------------------------------------
|
||||
class GeminiClient {
|
||||
constructor(modelId = 'gemini-2.5-flash', opts) {
|
||||
this.modelId = modelId;
|
||||
this.temperature = opts?.temperature ?? 0.2;
|
||||
}
|
||||
async chat(messages, tools, maxTokens = 8192) {
|
||||
const apiKey = process.env.GOOGLE_API_KEY;
|
||||
if (!apiKey)
|
||||
throw new Error('GOOGLE_API_KEY not set');
|
||||
const genai = new genai_1.GoogleGenAI({ apiKey });
|
||||
const systemMsg = messages.find(m => m.role === 'system');
|
||||
const nonSystem = messages.filter(m => m.role !== 'system');
|
||||
const functionDeclarations = (tools ?? []).map(t => ({
|
||||
name: t.function.name,
|
||||
description: t.function.description,
|
||||
parameters: t.function.parameters
|
||||
}));
|
||||
const response = await genai.models.generateContent({
|
||||
model: this.modelId,
|
||||
contents: toGeminiContents(nonSystem),
|
||||
config: {
|
||||
systemInstruction: systemMsg?.content ?? undefined,
|
||||
tools: functionDeclarations.length > 0 ? [{ functionDeclarations }] : undefined,
|
||||
temperature: this.temperature,
|
||||
maxOutputTokens: maxTokens
|
||||
}
|
||||
});
|
||||
const candidate = response.candidates?.[0];
|
||||
if (!candidate)
|
||||
throw new Error('No response from Gemini');
|
||||
const parts = candidate.content?.parts ?? [];
|
||||
const textContent = parts.filter(p => p.text).map(p => p.text).join('') || null;
|
||||
const fnCalls = parts.filter(p => p.functionCall);
|
||||
const tool_calls = fnCalls.map(p => ({
|
||||
id: `call_${(0, uuid_1.v4)().replace(/-/g, '').slice(0, 12)}`,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: p.functionCall.name ?? '',
|
||||
arguments: JSON.stringify(p.functionCall.args ?? {})
|
||||
}
|
||||
}));
|
||||
return {
|
||||
content: textContent,
|
||||
reasoning: null,
|
||||
tool_calls,
|
||||
finish_reason: fnCalls.length > 0 ? 'tool_calls' : 'stop'
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.GeminiClient = GeminiClient;
|
||||
/** Convert OpenAI message format → Gemini Content[] format */
|
||||
function toGeminiContents(messages) {
|
||||
const contents = [];
|
||||
for (const msg of messages) {
|
||||
if (msg.role === 'assistant') {
|
||||
const parts = [];
|
||||
if (msg.content)
|
||||
parts.push({ text: msg.content });
|
||||
for (const tc of msg.tool_calls ?? []) {
|
||||
parts.push({
|
||||
functionCall: {
|
||||
name: tc.function.name,
|
||||
args: JSON.parse(tc.function.arguments || '{}')
|
||||
}
|
||||
});
|
||||
}
|
||||
contents.push({ role: 'model', parts });
|
||||
}
|
||||
else if (msg.role === 'tool') {
|
||||
// Parse content back — could be JSON or plain text
|
||||
let resultValue = msg.content;
|
||||
try {
|
||||
resultValue = JSON.parse(msg.content ?? 'null');
|
||||
}
|
||||
catch { /* keep as string */ }
|
||||
contents.push({
|
||||
role: 'user',
|
||||
parts: [{
|
||||
functionResponse: {
|
||||
name: msg.name ?? 'tool',
|
||||
response: { result: resultValue }
|
||||
}
|
||||
}]
|
||||
});
|
||||
}
|
||||
else {
|
||||
contents.push({ role: 'user', parts: [{ text: msg.content ?? '' }] });
|
||||
}
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Anthropic Vertex client
|
||||
// Used for: claude-* models via Vertex AI (proper Anthropic Messages API)
|
||||
// Handles tool_calls by converting to/from Anthropic's tool_use blocks.
|
||||
// ---------------------------------------------------------------------------
|
||||
class AnthropicVertexClient {
|
||||
constructor(modelId, opts) {
|
||||
// Strip the "anthropic/" prefix if present — the SDK uses bare model names
|
||||
this.modelId = modelId.startsWith('anthropic/') ? modelId.slice(10) : modelId;
|
||||
this.projectId = opts?.projectId ?? process.env.GCP_PROJECT_ID ?? 'master-ai-484822';
|
||||
this.region = opts?.region ?? process.env.CLAUDE_REGION ?? 'us-east5';
|
||||
}
|
||||
buildClient() {
|
||||
const b64Key = process.env.GCP_SA_KEY_BASE64;
|
||||
if (b64Key) {
|
||||
try {
|
||||
const jsonStr = Buffer.from(b64Key, 'base64').toString('utf8');
|
||||
const credentials = JSON.parse(jsonStr);
|
||||
return new vertex_sdk_1.default({
|
||||
projectId: this.projectId,
|
||||
region: this.region,
|
||||
googleAuth: new google_auth_library_1.GoogleAuth({ credentials, scopes: ['https://www.googleapis.com/auth/cloud-platform'] }),
|
||||
});
|
||||
}
|
||||
catch {
|
||||
console.warn('[llm] AnthropicVertex: SA key decode failed, falling back to metadata server');
|
||||
}
|
||||
}
|
||||
return new vertex_sdk_1.default({ projectId: this.projectId, region: this.region });
|
||||
}
|
||||
async chat(messages, tools, maxTokens = 8192) {
|
||||
const client = this.buildClient();
|
||||
const system = messages.find(m => m.role === 'system')?.content ?? undefined;
|
||||
const nonSystem = messages.filter(m => m.role !== 'system');
|
||||
// Convert OpenAI message format → Anthropic format
|
||||
const anthropicMessages = nonSystem.map(m => {
|
||||
if (m.role === 'assistant') {
|
||||
const parts = [];
|
||||
if (m.content)
|
||||
parts.push({ type: 'text', text: m.content });
|
||||
for (const tc of m.tool_calls ?? []) {
|
||||
parts.push({
|
||||
type: 'tool_use',
|
||||
id: tc.id,
|
||||
name: tc.function.name,
|
||||
input: JSON.parse(tc.function.arguments || '{}'),
|
||||
});
|
||||
}
|
||||
return { role: 'assistant', content: parts.length === 1 && parts[0].type === 'text' ? parts[0].text : parts };
|
||||
}
|
||||
if (m.role === 'tool') {
|
||||
return {
|
||||
role: 'user',
|
||||
content: [{ type: 'tool_result', tool_use_id: m.tool_call_id, content: m.content ?? '' }],
|
||||
};
|
||||
}
|
||||
return { role: 'user', content: m.content ?? '' };
|
||||
});
|
||||
const anthropicTools = (tools ?? []).map(t => ({
|
||||
name: t.function.name,
|
||||
description: t.function.description,
|
||||
input_schema: t.function.parameters,
|
||||
}));
|
||||
const MAX_RETRIES = 4;
|
||||
const RETRY_STATUSES = new Set([429, 503]);
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const response = await client.messages.create({
|
||||
model: this.modelId,
|
||||
max_tokens: maxTokens,
|
||||
system: system ?? undefined,
|
||||
messages: anthropicMessages,
|
||||
tools: anthropicTools.length > 0 ? anthropicTools : undefined,
|
||||
});
|
||||
const textContent = response.content
|
||||
.filter((b) => b.type === 'text')
|
||||
.map((b) => b.text)
|
||||
.join('') || null;
|
||||
const tool_calls = response.content
|
||||
.filter((b) => b.type === 'tool_use')
|
||||
.map((b) => ({
|
||||
id: b.id,
|
||||
type: 'function',
|
||||
function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) },
|
||||
}));
|
||||
return {
|
||||
content: textContent,
|
||||
reasoning: null,
|
||||
tool_calls,
|
||||
finish_reason: response.stop_reason === 'tool_use' ? 'tool_calls' : 'stop',
|
||||
usage: response.usage
|
||||
? { prompt_tokens: response.usage.input_tokens, completion_tokens: response.usage.output_tokens, total_tokens: response.usage.input_tokens + response.usage.output_tokens }
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
const status = err?.status ?? err?.statusCode ?? 0;
|
||||
if (RETRY_STATUSES.has(status) && attempt < MAX_RETRIES) {
|
||||
const waitMs = Math.min(2 ** attempt * 2000 + Math.random() * 500, 30000);
|
||||
console.warn(`[llm] Anthropic Vertex ${status} on attempt ${attempt + 1}/${MAX_RETRIES + 1} — retrying in ${Math.round(waitMs / 1000)}s`);
|
||||
await new Promise(r => setTimeout(r, waitMs));
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Anthropic Vertex error: ${err?.message ?? String(err)}`);
|
||||
}
|
||||
}
|
||||
throw new Error('Anthropic Vertex: exceeded max retries');
|
||||
}
|
||||
}
|
||||
exports.AnthropicVertexClient = AnthropicVertexClient;
|
||||
const TIER_MODELS = {
|
||||
A: process.env.TIER_A_MODEL ?? 'gemini-2.5-flash',
|
||||
B: process.env.TIER_B_MODEL ?? 'claude-sonnet-4-6',
|
||||
C: process.env.TIER_C_MODEL ?? 'claude-sonnet-4-6'
|
||||
};
|
||||
function createLLM(modelOrTier, opts) {
|
||||
const modelId = (modelOrTier === 'A' || modelOrTier === 'B' || modelOrTier === 'C')
|
||||
? TIER_MODELS[modelOrTier]
|
||||
: modelOrTier;
|
||||
if (modelId.startsWith('gemini-')) {
|
||||
return new GeminiClient(modelId, opts);
|
||||
}
|
||||
if (modelId.startsWith('anthropic/') || modelId.startsWith('claude-')) {
|
||||
return new AnthropicVertexClient(modelId);
|
||||
}
|
||||
return new VertexOpenAIClient(modelId, { temperature: opts?.temperature });
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper — convert our ToolDefinition[] → LLMTool[] (OpenAI format)
|
||||
// ---------------------------------------------------------------------------
|
||||
function toOAITools(tools) {
|
||||
return tools.map(t => ({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
parameters: t.parameters
|
||||
}
|
||||
}));
|
||||
}
|
||||
2
vibn-agent-runner/dist/mcp/agent-server.d.ts
vendored
Normal file
2
vibn-agent-runner/dist/mcp/agent-server.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
export {};
|
||||
125
vibn-agent-runner/dist/mcp/agent-server.js
vendored
Normal file
125
vibn-agent-runner/dist/mcp/agent-server.js
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// vibn-agent-mcp
|
||||
// -----------------------------------------------------------------------------
|
||||
// Stdio MCP server exposing the vibn-agent-runner sub-agent orchestration API.
|
||||
// This lets any MCP-speaking client (Goose, Claude Desktop, Cursor, etc.)
|
||||
// spawn Coder / PM / Marketing jobs against the vibn-agent-runner HTTP service
|
||||
// and poll their status.
|
||||
//
|
||||
// Config (env):
|
||||
// AGENT_RUNNER_URL (default: http://localhost:3333) — URL of the runner
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
||||
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
||||
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
||||
const api = __importStar(require("../tools/agent-api"));
|
||||
function loadConfig() {
|
||||
const runnerUrl = process.env.AGENT_RUNNER_URL?.trim() || 'http://localhost:3333';
|
||||
return { runnerUrl };
|
||||
}
|
||||
const TOOL_DEFINITIONS = [
|
||||
{
|
||||
name: 'spawn_agent',
|
||||
description: 'Dispatch a sub-agent job to run in the background on the vibn-agent-runner. Returns a job ID.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
agent: { type: 'string', description: '"Coder", "PM", or "Marketing"' },
|
||||
task: { type: 'string', description: 'Detailed task description for the agent' },
|
||||
repo: { type: 'string', description: 'Gitea repo in "owner/name" format' },
|
||||
},
|
||||
required: ['agent', 'task', 'repo'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_job_status',
|
||||
description: 'Check the status of a previously spawned agent job.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: { type: 'string', description: 'Job ID returned by spawn_agent' },
|
||||
},
|
||||
required: ['job_id'],
|
||||
},
|
||||
},
|
||||
];
|
||||
async function dispatch(cfg, name, args) {
|
||||
switch (name) {
|
||||
case 'spawn_agent':
|
||||
return api.spawnAgent(cfg, {
|
||||
agent: String(args.agent),
|
||||
task: String(args.task),
|
||||
repo: String(args.repo),
|
||||
});
|
||||
case 'get_job_status':
|
||||
return api.getJobStatus(cfg, String(args.job_id));
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
function buildServer(cfg) {
|
||||
const server = new index_js_1.Server({ name: 'vibn-agent-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
|
||||
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: TOOL_DEFINITIONS }));
|
||||
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
||||
const name = request.params.name;
|
||||
const args = (request.params.arguments ?? {});
|
||||
try {
|
||||
const result = await dispatch(cfg, name, args);
|
||||
return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { isError: true, content: [{ type: 'text', text: `Error: ${message}` }] };
|
||||
}
|
||||
});
|
||||
return server;
|
||||
}
|
||||
async function main() {
|
||||
const cfg = loadConfig();
|
||||
const server = buildServer(cfg);
|
||||
const transport = new stdio_js_1.StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[vibn-agent-mcp] ready — ${TOOL_DEFINITIONS.length} tools exposed (runner=${cfg.runnerUrl})`);
|
||||
}
|
||||
main().catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[vibn-agent-mcp] fatal:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
1
vibn-agent-runner/dist/mcp/coolify-server.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/mcp/coolify-server.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
198
vibn-agent-runner/dist/mcp/coolify-server.js
vendored
Normal file
198
vibn-agent-runner/dist/mcp/coolify-server.js
vendored
Normal file
@@ -0,0 +1,198 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Vibn Coolify MCP Server
|
||||
//
|
||||
// Exposes the Coolify tools from src/tools/coolify-api.ts over the Model Context
|
||||
// Protocol via stdio. Same security guardrails, same code path as the in-process
|
||||
// agent runner — just accessible to any MCP-speaking client (Goose,
|
||||
// Claude Code, Cursor, future harnesses).
|
||||
//
|
||||
// Launch:
|
||||
// COOLIFY_API_URL=https://coolify.vibnai.com COOLIFY_API_TOKEN=... \
|
||||
// node dist/mcp/coolify-server.js
|
||||
//
|
||||
// The server speaks the MCP stdio transport on its stdin/stdout. Any logs go to
|
||||
// stderr so they don't corrupt the protocol stream.
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
||||
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
||||
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
||||
const api = __importStar(require("../tools/coolify-api"));
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config — single source of truth, loaded once at startup
|
||||
// ---------------------------------------------------------------------------
|
||||
function loadConfig() {
|
||||
const apiUrl = process.env.COOLIFY_API_URL;
|
||||
const apiToken = process.env.COOLIFY_API_TOKEN;
|
||||
if (!apiUrl)
|
||||
throw new Error('COOLIFY_API_URL env var is required');
|
||||
if (!apiToken)
|
||||
throw new Error('COOLIFY_API_TOKEN env var is required');
|
||||
return { apiUrl, apiToken };
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool surface — names, descriptions, and JSON Schema kept byte-identical to
|
||||
// the in-process registrations in tools/coolify.ts so callers get the same
|
||||
// behavior regardless of transport.
|
||||
// ---------------------------------------------------------------------------
|
||||
const TOOL_DEFINITIONS = [
|
||||
{
|
||||
name: 'coolify_list_projects',
|
||||
description: 'List all projects in the Coolify instance. Returns project names and UUIDs.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'coolify_list_applications',
|
||||
description: 'List applications in a Coolify project.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project_uuid: { type: 'string', description: 'Project UUID from coolify_list_projects' },
|
||||
},
|
||||
required: ['project_uuid'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'coolify_deploy',
|
||||
description: 'Trigger a deployment for a Coolify application.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
application_uuid: { type: 'string', description: 'Application UUID to deploy' },
|
||||
},
|
||||
required: ['application_uuid'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'coolify_get_logs',
|
||||
description: 'Get recent deployment logs for a Coolify application.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
application_uuid: { type: 'string', description: 'Application UUID' },
|
||||
},
|
||||
required: ['application_uuid'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_all_apps',
|
||||
description: 'List all Coolify applications across all projects with their status (running/stopped/error) and domain.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'get_app_status',
|
||||
description: 'Get the current deployment status and recent logs for a specific Coolify application by name or UUID.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
app_name: { type: 'string', description: 'Application name (e.g. "vibn-frontend") or UUID' },
|
||||
},
|
||||
required: ['app_name'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'deploy_app',
|
||||
description: 'Trigger a Coolify deployment for an app by name. Use after an agent commits code.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
app_name: { type: 'string', description: 'Application name (e.g. "vibn-frontend")' },
|
||||
},
|
||||
required: ['app_name'],
|
||||
},
|
||||
},
|
||||
];
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch
|
||||
// ---------------------------------------------------------------------------
|
||||
async function dispatch(cfg, name, args) {
|
||||
switch (name) {
|
||||
case 'coolify_list_projects':
|
||||
return api.listProjects(cfg);
|
||||
case 'coolify_list_applications':
|
||||
return api.listApplications(cfg, String(args.project_uuid));
|
||||
case 'coolify_deploy':
|
||||
return api.deploy(cfg, String(args.application_uuid));
|
||||
case 'coolify_get_logs':
|
||||
return api.getLogs(cfg, String(args.application_uuid));
|
||||
case 'list_all_apps':
|
||||
return api.listAllApps(cfg);
|
||||
case 'get_app_status':
|
||||
return api.getAppStatus(cfg, String(args.app_name));
|
||||
case 'deploy_app':
|
||||
return api.deployApp(cfg, String(args.app_name));
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Server
|
||||
// ---------------------------------------------------------------------------
|
||||
function buildServer(cfg) {
|
||||
const server = new index_js_1.Server({ name: 'vibn-coolify', version: '0.1.0' }, { capabilities: { tools: {} } });
|
||||
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
||||
tools: TOOL_DEFINITIONS.map(t => ({ ...t })),
|
||||
}));
|
||||
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
||||
const { name, arguments: args = {} } = req.params;
|
||||
try {
|
||||
const result = await dispatch(cfg, name, args);
|
||||
const text = typeof result === 'string' ? result : JSON.stringify(result);
|
||||
return { content: [{ type: 'text', text }] };
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
return server;
|
||||
}
|
||||
async function main() {
|
||||
const cfg = loadConfig();
|
||||
const server = buildServer(cfg);
|
||||
const transport = new stdio_js_1.StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
// stderr so we don't corrupt the stdio MCP stream
|
||||
console.error(`[vibn-coolify-mcp] ready — ${TOOL_DEFINITIONS.length} tools exposed (coolify=${cfg.apiUrl})`);
|
||||
}
|
||||
main().catch((err) => {
|
||||
console.error('[vibn-coolify-mcp] fatal:', err instanceof Error ? err.stack : err);
|
||||
process.exit(1);
|
||||
});
|
||||
1
vibn-agent-runner/dist/mcp/gitea-server.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/mcp/gitea-server.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
186
vibn-agent-runner/dist/mcp/gitea-server.js
vendored
Normal file
186
vibn-agent-runner/dist/mcp/gitea-server.js
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Vibn Gitea MCP Server
|
||||
//
|
||||
// Exposes the Gitea tools from src/tools/gitea-api.ts over the Model Context
|
||||
// Protocol via stdio. Same security guardrails, same code path as the
|
||||
// in-process agent runner — accessible to any MCP-speaking client.
|
||||
//
|
||||
// Launch:
|
||||
// GITEA_API_URL=https://git.vibnai.com GITEA_API_TOKEN=... GITEA_USERNAME=mark \
|
||||
// node dist/mcp/gitea-server.js
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
||||
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
||||
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
||||
const api = __importStar(require("../tools/gitea-api"));
|
||||
function loadConfig() {
|
||||
const apiUrl = process.env.GITEA_API_URL;
|
||||
const apiToken = process.env.GITEA_API_TOKEN;
|
||||
if (!apiUrl)
|
||||
throw new Error('GITEA_API_URL env var is required');
|
||||
if (!apiToken)
|
||||
throw new Error('GITEA_API_TOKEN env var is required');
|
||||
return { apiUrl, apiToken, username: process.env.GITEA_USERNAME };
|
||||
}
|
||||
const TOOL_DEFINITIONS = [
|
||||
{
|
||||
name: 'gitea_create_issue',
|
||||
description: 'Create a new issue in a Gitea repository.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repo: { type: 'string', description: 'Repository in "owner/name" format' },
|
||||
title: { type: 'string', description: 'Issue title' },
|
||||
body: { type: 'string', description: 'Issue body (markdown)' },
|
||||
labels: { type: 'array', items: { type: 'string' }, description: 'Optional label names' },
|
||||
},
|
||||
required: ['repo', 'title', 'body'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gitea_list_issues',
|
||||
description: 'List open issues in a Gitea repository.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repo: { type: 'string', description: 'Repository in "owner/name" format' },
|
||||
state: { type: 'string', description: '"open", "closed", or "all". Default: "open"' },
|
||||
},
|
||||
required: ['repo'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gitea_close_issue',
|
||||
description: 'Close an issue in a Gitea repository.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repo: { type: 'string', description: 'Repository in "owner/name" format' },
|
||||
issue_number: { type: 'number', description: 'Issue number to close' },
|
||||
},
|
||||
required: ['repo', 'issue_number'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_repos',
|
||||
description: 'List all Git repositories in the Gitea organization. Returns repo names, descriptions, and last update time.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'list_all_issues',
|
||||
description: 'List open issues across all repos or a specific repo. Use this to understand what work is queued or in progress.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repo: { type: 'string', description: 'Optional: "owner/name" to scope to one repo. Omit for all repos.' },
|
||||
state: { type: 'string', description: '"open", "closed", or "all". Default: "open"' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'read_repo_file',
|
||||
description: 'Read a file from any Gitea repository without cloning it. Useful for understanding project structure.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repo: { type: 'string', description: 'Repo in "owner/name" format' },
|
||||
path: { type: 'string', description: 'File path within the repo (e.g. "src/app/page.tsx")' },
|
||||
},
|
||||
required: ['repo', 'path'],
|
||||
},
|
||||
},
|
||||
];
|
||||
async function dispatch(cfg, name, args) {
|
||||
switch (name) {
|
||||
case 'gitea_create_issue':
|
||||
return api.createIssue(cfg, {
|
||||
repo: String(args.repo),
|
||||
title: String(args.title),
|
||||
body: String(args.body),
|
||||
labels: Array.isArray(args.labels) ? args.labels : undefined,
|
||||
});
|
||||
case 'gitea_list_issues':
|
||||
return api.listIssues(cfg, String(args.repo), String(args.state || 'open'));
|
||||
case 'gitea_close_issue':
|
||||
return api.closeIssue(cfg, String(args.repo), Number(args.issue_number));
|
||||
case 'list_repos':
|
||||
return api.listRepos(cfg);
|
||||
case 'list_all_issues':
|
||||
return api.listAllIssues(cfg, {
|
||||
repo: args.repo ? String(args.repo) : undefined,
|
||||
state: args.state ? String(args.state) : undefined,
|
||||
});
|
||||
case 'read_repo_file':
|
||||
return api.readRepoFile(cfg, String(args.repo), String(args.path));
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
function buildServer(cfg) {
|
||||
const server = new index_js_1.Server({ name: 'vibn-gitea', version: '0.1.0' }, { capabilities: { tools: {} } });
|
||||
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
||||
tools: TOOL_DEFINITIONS.map(t => ({ ...t })),
|
||||
}));
|
||||
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
||||
const { name, arguments: args = {} } = req.params;
|
||||
try {
|
||||
const result = await dispatch(cfg, name, args);
|
||||
const text = typeof result === 'string' ? result : JSON.stringify(result);
|
||||
return { content: [{ type: 'text', text }] };
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
return server;
|
||||
}
|
||||
async function main() {
|
||||
const cfg = loadConfig();
|
||||
const server = buildServer(cfg);
|
||||
const transport = new stdio_js_1.StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error(`[vibn-gitea-mcp] ready — ${TOOL_DEFINITIONS.length} tools exposed (gitea=${cfg.apiUrl})`);
|
||||
}
|
||||
main().catch((err) => {
|
||||
console.error('[vibn-gitea-mcp] fatal:', err instanceof Error ? err.stack : err);
|
||||
process.exit(1);
|
||||
});
|
||||
2
vibn-agent-runner/dist/mcp/vibn-platform-server.d.ts
vendored
Normal file
2
vibn-agent-runner/dist/mcp/vibn-platform-server.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
export {};
|
||||
201
vibn-agent-runner/dist/mcp/vibn-platform-server.js
vendored
Normal file
201
vibn-agent-runner/dist/mcp/vibn-platform-server.js
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// vibn-platform-mcp
|
||||
// -----------------------------------------------------------------------------
|
||||
// Stdio MCP server exposing Vibn platform primitives:
|
||||
// - save_memory → persists facts into a per-session in-memory store
|
||||
// - list_memory → inspect what has been saved this session (MCP-only)
|
||||
// - list_skills → enumerate .skills/ in a Gitea repo
|
||||
// - get_skill → read a specific SKILL.md
|
||||
// - finalize_prd → save a completed PRD keyed by SESSION_KEY
|
||||
// - get_prd → read back the saved PRD (MCP-only convenience)
|
||||
// - web_search → DuckDuckGo HTML search
|
||||
//
|
||||
// NOTE: The in-process agent-runner collects memory into ToolContext and
|
||||
// consumes the PRD via the module-level prdStore. When the same logic is
|
||||
// exposed over MCP, there is no shared process memory with the agent-runner,
|
||||
// so this server maintains its own session-scoped stores. Set SESSION_KEY to
|
||||
// give each MCP client a stable key into those stores.
|
||||
//
|
||||
// Config (env):
|
||||
// SESSION_KEY (optional) — session scope for memory + PRD stores
|
||||
// (defaults to "default")
|
||||
// GITEA_API_URL (optional) — required for list_skills / get_skill
|
||||
// GITEA_API_TOKEN (optional) — required for list_skills / get_skill
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
||||
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
||||
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
||||
const memoryApi = __importStar(require("../tools/memory-api"));
|
||||
const skillsApi = __importStar(require("../tools/skills-api"));
|
||||
const prdApi = __importStar(require("../tools/prd-api"));
|
||||
const searchApi = __importStar(require("../tools/search-api"));
|
||||
function loadConfig() {
|
||||
const sessionKey = process.env.SESSION_KEY?.trim() || 'default';
|
||||
const giteaUrl = process.env.GITEA_API_URL;
|
||||
const giteaToken = process.env.GITEA_API_TOKEN;
|
||||
const gitea = giteaUrl && giteaToken ? { apiUrl: giteaUrl, apiToken: giteaToken } : undefined;
|
||||
return { sessionKey, gitea };
|
||||
}
|
||||
const TOOL_DEFINITIONS = [
|
||||
{
|
||||
name: 'save_memory',
|
||||
description: 'Persist an important fact about this project to long-term memory within this MCP session.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string', description: 'Short unique label (e.g. "primary_language", "auth_strategy")' },
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['tech_stack', 'decision', 'feature', 'goal', 'constraint', 'note'],
|
||||
description: 'Category of the memory item',
|
||||
},
|
||||
value: { type: 'string', description: 'The fact to remember (1-3 sentences)' },
|
||||
},
|
||||
required: ['key', 'type', 'value'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_memory',
|
||||
description: 'List all memory entries saved in the current session.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'list_skills',
|
||||
description: 'List available skills for a project repo. Skills are stored in .skills/<name>/SKILL.md. Requires Gitea credentials.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { repo: { type: 'string', description: 'Repo in "owner/name" format' } },
|
||||
required: ['repo'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_skill',
|
||||
description: 'Read the full content of a specific skill from a project repo. Requires Gitea credentials.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repo: { type: 'string', description: 'Repo in "owner/name" format' },
|
||||
skill_name: { type: 'string', description: 'Skill name (directory inside .skills/)' },
|
||||
},
|
||||
required: ['repo', 'skill_name'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'finalize_prd',
|
||||
description: 'Save a completed PRD document for this session.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { content: { type: 'string', description: 'The complete PRD in markdown' } },
|
||||
required: ['content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_prd',
|
||||
description: 'Read back the PRD saved for this session, or null if none saved yet.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'web_search',
|
||||
description: 'Search the web via DuckDuckGo HTML endpoint. Returns titles + snippets for the top results.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { query: { type: 'string', description: 'The search query' } },
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
];
|
||||
async function dispatch(cfg, name, args) {
|
||||
switch (name) {
|
||||
case 'save_memory':
|
||||
return memoryApi.saveMemoryToStore(cfg.sessionKey, {
|
||||
key: String(args.key),
|
||||
type: String(args.type),
|
||||
value: String(args.value),
|
||||
});
|
||||
case 'list_memory':
|
||||
return { sessionKey: cfg.sessionKey, entries: memoryApi.listMemoryFromStore(cfg.sessionKey) };
|
||||
case 'list_skills':
|
||||
if (!cfg.gitea)
|
||||
return { error: 'list_skills requires GITEA_API_URL and GITEA_API_TOKEN.' };
|
||||
return skillsApi.listSkills(cfg.gitea, String(args.repo));
|
||||
case 'get_skill':
|
||||
if (!cfg.gitea)
|
||||
return { error: 'get_skill requires GITEA_API_URL and GITEA_API_TOKEN.' };
|
||||
return skillsApi.getSkill(cfg.gitea, String(args.repo), String(args.skill_name));
|
||||
case 'finalize_prd':
|
||||
return prdApi.finalizePrd(cfg.sessionKey, String(args.content));
|
||||
case 'get_prd':
|
||||
return { sessionKey: cfg.sessionKey, content: prdApi.getPrd(cfg.sessionKey) };
|
||||
case 'web_search':
|
||||
return searchApi.webSearch(String(args.query));
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
function buildServer(cfg) {
|
||||
const server = new index_js_1.Server({ name: 'vibn-platform-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
|
||||
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: TOOL_DEFINITIONS }));
|
||||
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
||||
const name = request.params.name;
|
||||
const args = (request.params.arguments ?? {});
|
||||
try {
|
||||
const result = await dispatch(cfg, name, args);
|
||||
return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { isError: true, content: [{ type: 'text', text: `Error: ${message}` }] };
|
||||
}
|
||||
});
|
||||
return server;
|
||||
}
|
||||
async function main() {
|
||||
const cfg = loadConfig();
|
||||
const server = buildServer(cfg);
|
||||
const transport = new stdio_js_1.StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[vibn-platform-mcp] ready — ${TOOL_DEFINITIONS.length} tools exposed ` +
|
||||
`(session=${cfg.sessionKey}, gitea=${cfg.gitea ? 'enabled' : 'disabled'})`);
|
||||
}
|
||||
main().catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[vibn-platform-mcp] fatal:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
2
vibn-agent-runner/dist/mcp/workspace-server.d.ts
vendored
Normal file
2
vibn-agent-runner/dist/mcp/workspace-server.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
export {};
|
||||
233
vibn-agent-runner/dist/mcp/workspace-server.js
vendored
Normal file
233
vibn-agent-runner/dist/mcp/workspace-server.js
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// vibn-workspace-mcp
|
||||
// -----------------------------------------------------------------------------
|
||||
// Stdio MCP server exposing the coding-agent workspace toolkit:
|
||||
// - Filesystem primitives (read/write/replace/list/find/search)
|
||||
// - Shell execution (120s timeout, blocked-command guard)
|
||||
// - Authenticated git commit + push with protected-repo guard
|
||||
//
|
||||
// Each server instance is scoped to a single WORKSPACE_ROOT. To operate against
|
||||
// multiple workspaces, spawn multiple MCP server instances (one per workspace).
|
||||
// This mirrors how Goose / Claude Desktop / Cursor MCP configs work in practice.
|
||||
//
|
||||
// Config (env):
|
||||
// WORKSPACE_ROOT (required) — absolute path to the workspace
|
||||
// GITEA_API_URL (optional) — required if caller uses git_commit_and_push
|
||||
// GITEA_API_TOKEN (optional) — required if caller uses git_commit_and_push
|
||||
// GITEA_USERNAME (optional) — required if caller uses git_commit_and_push
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const path = __importStar(require("path"));
|
||||
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
||||
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
||||
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
||||
const fileApi = __importStar(require("../tools/file-api"));
|
||||
const shellApi = __importStar(require("../tools/shell-api"));
|
||||
const gitApi = __importStar(require("../tools/git-api"));
|
||||
function loadConfig() {
|
||||
const workspaceRoot = process.env.WORKSPACE_ROOT;
|
||||
if (!workspaceRoot) {
|
||||
throw new Error('WORKSPACE_ROOT is required (absolute path to the workspace to operate on).');
|
||||
}
|
||||
const absWorkspace = path.resolve(workspaceRoot);
|
||||
const giteaUrl = process.env.GITEA_API_URL;
|
||||
const giteaToken = process.env.GITEA_API_TOKEN;
|
||||
const giteaUser = process.env.GITEA_USERNAME;
|
||||
const gitea = giteaUrl && giteaToken && giteaUser
|
||||
? { apiUrl: giteaUrl, apiToken: giteaToken, username: giteaUser }
|
||||
: undefined;
|
||||
return { workspaceRoot: absWorkspace, gitea };
|
||||
}
|
||||
const TOOL_DEFINITIONS = [
|
||||
{
|
||||
name: 'read_file',
|
||||
description: 'Read the complete content of a file in the workspace. Always read before editing.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Relative path from workspace root (e.g. "src/index.ts")' },
|
||||
},
|
||||
required: ['path'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'write_file',
|
||||
description: 'Write complete content to a file. Creates parent directories if needed. Overwrites existing files.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Relative path from workspace root' },
|
||||
content: { type: 'string', description: 'Complete new file content' },
|
||||
},
|
||||
required: ['path', 'content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'replace_in_file',
|
||||
description: 'Replace an exact string in a file. The old_content must match character-for-character. Read the file first.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Relative path from workspace root' },
|
||||
old_content: { type: 'string', description: 'Exact text to replace' },
|
||||
new_content: { type: 'string', description: 'Replacement text' },
|
||||
},
|
||||
required: ['path', 'old_content', 'new_content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_directory',
|
||||
description: 'List files and subdirectories in a directory. Directories have trailing "/".',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Relative path from workspace root. Use "." for root.' },
|
||||
},
|
||||
required: ['path'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'find_files',
|
||||
description: 'Find files matching a glob pattern in the workspace. Returns up to 200 relative paths.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
pattern: { type: 'string', description: 'Glob pattern e.g. "**/*.ts", "src/**/*.test.js"' },
|
||||
},
|
||||
required: ['pattern'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'search_code',
|
||||
description: 'Search file contents for a string using ripgrep. Returns file path, line number, and matching line.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Search term (fixed-string)' },
|
||||
file_extensions: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Optional: limit to these extensions e.g. ["ts","js"]',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'execute_command',
|
||||
description: 'Run a shell command in the workspace and return stdout + stderr. 120s timeout.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
command: { type: 'string', description: 'Shell command to run' },
|
||||
working_directory: { type: 'string', description: 'Optional: relative subdirectory to run in' },
|
||||
},
|
||||
required: ['command'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'git_commit_and_push',
|
||||
description: 'Stage all changes, commit with a message, and push to the remote using configured Gitea credentials. Blocks pushes to protected platform repos.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: { type: 'string', description: 'Commit message describing the changes made' },
|
||||
},
|
||||
required: ['message'],
|
||||
},
|
||||
},
|
||||
];
|
||||
async function dispatch(cfg, name, args) {
|
||||
switch (name) {
|
||||
case 'read_file':
|
||||
return fileApi.readFile(cfg.workspaceRoot, String(args.path));
|
||||
case 'write_file':
|
||||
return fileApi.writeFile(cfg.workspaceRoot, String(args.path), String(args.content));
|
||||
case 'replace_in_file':
|
||||
return fileApi.replaceInFile(cfg.workspaceRoot, String(args.path), String(args.old_content), String(args.new_content));
|
||||
case 'list_directory':
|
||||
return fileApi.listDirectory(cfg.workspaceRoot, String(args.path));
|
||||
case 'find_files':
|
||||
return fileApi.findFiles(cfg.workspaceRoot, String(args.pattern));
|
||||
case 'search_code': {
|
||||
const exts = Array.isArray(args.file_extensions) ? args.file_extensions : undefined;
|
||||
return fileApi.searchCode(cfg.workspaceRoot, String(args.query), exts);
|
||||
}
|
||||
case 'execute_command':
|
||||
return shellApi.executeCommand(cfg.workspaceRoot, String(args.command), args.working_directory ? String(args.working_directory) : undefined);
|
||||
case 'git_commit_and_push': {
|
||||
if (!cfg.gitea) {
|
||||
return { error: 'git_commit_and_push requires GITEA_API_URL, GITEA_API_TOKEN, and GITEA_USERNAME environment variables.' };
|
||||
}
|
||||
return gitApi.gitCommitAndPush(cfg.workspaceRoot, String(args.message), cfg.gitea);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
function buildServer(cfg) {
|
||||
const server = new index_js_1.Server({ name: 'vibn-workspace-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
|
||||
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: TOOL_DEFINITIONS }));
|
||||
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
||||
const name = request.params.name;
|
||||
const args = (request.params.arguments ?? {});
|
||||
try {
|
||||
const result = await dispatch(cfg, name, args);
|
||||
return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { isError: true, content: [{ type: 'text', text: `Error: ${message}` }] };
|
||||
}
|
||||
});
|
||||
return server;
|
||||
}
|
||||
async function main() {
|
||||
const cfg = loadConfig();
|
||||
const server = buildServer(cfg);
|
||||
const transport = new stdio_js_1.StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[vibn-workspace-mcp] ready — ${TOOL_DEFINITIONS.length} tools exposed ` +
|
||||
`(workspace=${cfg.workspaceRoot}, git=${cfg.gitea ? 'enabled' : 'disabled'})`);
|
||||
}
|
||||
main().catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[vibn-workspace-mcp] fatal:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
27
vibn-agent-runner/dist/orchestrator.d.ts
vendored
Normal file
27
vibn-agent-runner/dist/orchestrator.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import { LLMMessage } from './llm';
|
||||
import { ToolContext, MemoryUpdate } from './tools';
|
||||
export declare function listSessions(): {
|
||||
id: string;
|
||||
messages: number;
|
||||
createdAt: string;
|
||||
lastActiveAt: string;
|
||||
}[];
|
||||
export declare function clearSession(sessionId: string): void;
|
||||
export interface ChatResult {
|
||||
reply: string;
|
||||
reasoning: string | null;
|
||||
sessionId: string;
|
||||
turns: number;
|
||||
toolCalls: string[];
|
||||
model: string;
|
||||
/** Updated conversation history — caller should persist this */
|
||||
history: LLMMessage[];
|
||||
/** Knowledge items the AI chose to save this turn */
|
||||
memoryUpdates: MemoryUpdate[];
|
||||
}
|
||||
export declare function orchestratorChat(sessionId: string, userMessage: string, ctx: ToolContext, opts?: {
|
||||
/** Pre-load history from DB — replaces in-memory session history */
|
||||
preloadedHistory?: LLMMessage[];
|
||||
/** Knowledge items to inject as context at start of conversation */
|
||||
knowledgeContext?: string;
|
||||
}): Promise<ChatResult>;
|
||||
131
vibn-agent-runner/dist/orchestrator.js
vendored
Normal file
131
vibn-agent-runner/dist/orchestrator.js
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.listSessions = listSessions;
|
||||
exports.clearSession = clearSession;
|
||||
exports.orchestratorChat = orchestratorChat;
|
||||
const llm_1 = require("./llm");
|
||||
const tools_1 = require("./tools");
|
||||
const loader_1 = require("./prompts/loader");
|
||||
const MAX_TURNS = 20;
|
||||
const sessions = new Map();
|
||||
function getOrCreateSession(sessionId) {
|
||||
if (!sessions.has(sessionId)) {
|
||||
sessions.set(sessionId, {
|
||||
id: sessionId,
|
||||
history: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
lastActiveAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
const session = sessions.get(sessionId);
|
||||
session.lastActiveAt = new Date().toISOString();
|
||||
return session;
|
||||
}
|
||||
function listSessions() {
|
||||
return Array.from(sessions.values()).map(s => ({
|
||||
id: s.id,
|
||||
messages: s.history.length,
|
||||
createdAt: s.createdAt,
|
||||
lastActiveAt: s.lastActiveAt
|
||||
}));
|
||||
}
|
||||
function clearSession(sessionId) {
|
||||
sessions.delete(sessionId);
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main orchestrator chat — uses GLM-5 (Tier B) by default
|
||||
// ---------------------------------------------------------------------------
|
||||
async function orchestratorChat(sessionId, userMessage, ctx, opts) {
|
||||
const modelId = process.env.ORCHESTRATOR_MODEL ?? 'B'; // Tier B = GLM-5
|
||||
const llm = (0, llm_1.createLLM)(modelId, { temperature: 0.3 });
|
||||
const session = getOrCreateSession(sessionId);
|
||||
// Seed session from DB history if provided and session is fresh
|
||||
if (opts?.preloadedHistory && opts.preloadedHistory.length > 0 && session.history.length === 0) {
|
||||
session.history = [...opts.preloadedHistory];
|
||||
}
|
||||
const oaiTools = (0, llm_1.toOAITools)(tools_1.ALL_TOOLS);
|
||||
// Append user message
|
||||
session.history.push({ role: 'user', content: userMessage });
|
||||
let turn = 0;
|
||||
let finalReply = '';
|
||||
let finalReasoning = null;
|
||||
const toolCallNames = [];
|
||||
// Resolve system prompt from template — {{knowledge}} injects project/COO context
|
||||
const systemContent = (0, loader_1.resolvePrompt)('orchestrator', {
|
||||
knowledge: opts?.knowledgeContext ?? ''
|
||||
});
|
||||
// Build messages with system prompt prepended; keep last 40 for cost control
|
||||
const buildMessages = () => [
|
||||
{ role: 'system', content: systemContent },
|
||||
...session.history.slice(-40)
|
||||
];
|
||||
while (turn < MAX_TURNS) {
|
||||
turn++;
|
||||
const response = await llm.chat(buildMessages(), oaiTools, 4096);
|
||||
// If GLM-5 is still reasoning (content null, finish_reason length) give it more tokens
|
||||
if (response.content === null && response.tool_calls.length === 0 && response.finish_reason === 'length') {
|
||||
// Retry with more tokens — model hit max_tokens during reasoning
|
||||
const retry = await llm.chat(buildMessages(), oaiTools, 8192);
|
||||
Object.assign(response, retry);
|
||||
}
|
||||
// Record reasoning for the final turn (informational, not stored in history)
|
||||
if (response.reasoning)
|
||||
finalReasoning = response.reasoning;
|
||||
// Only push assistant message if it has actual content or tool calls;
|
||||
// skip empty turns that result from mid-reasoning token exhaustion.
|
||||
const hasContent = response.content !== null && response.content !== '';
|
||||
const hasToolCalls = response.tool_calls.length > 0;
|
||||
if (hasContent || hasToolCalls) {
|
||||
const assistantMsg = {
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
tool_calls: hasToolCalls ? response.tool_calls : undefined
|
||||
};
|
||||
session.history.push(assistantMsg);
|
||||
}
|
||||
// No tool calls — we have the final answer
|
||||
if (!hasToolCalls) {
|
||||
finalReply = response.content ?? '';
|
||||
break;
|
||||
}
|
||||
// Execute each tool call and collect results
|
||||
for (const tc of response.tool_calls) {
|
||||
const fnName = tc.function.name;
|
||||
let fnArgs = {};
|
||||
try {
|
||||
fnArgs = JSON.parse(tc.function.arguments || '{}');
|
||||
}
|
||||
catch { /* bad JSON */ }
|
||||
toolCallNames.push(fnName);
|
||||
let result;
|
||||
try {
|
||||
result = await (0, tools_1.executeTool)(fnName, fnArgs, ctx);
|
||||
}
|
||||
catch (err) {
|
||||
result = { error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
// Add tool result to history
|
||||
session.history.push({
|
||||
role: 'tool',
|
||||
tool_call_id: tc.id,
|
||||
name: fnName,
|
||||
content: typeof result === 'string' ? result : JSON.stringify(result)
|
||||
});
|
||||
}
|
||||
}
|
||||
if (turn >= MAX_TURNS && !finalReply) {
|
||||
finalReply = 'Hit the turn limit. Try a more specific request.';
|
||||
}
|
||||
return {
|
||||
reply: finalReply,
|
||||
reasoning: finalReasoning,
|
||||
sessionId,
|
||||
turns: turn,
|
||||
toolCalls: toolCallNames,
|
||||
model: llm.modelId,
|
||||
history: session.history
|
||||
.filter(m => m.role !== 'assistant' || m.content || m.tool_calls?.length)
|
||||
.slice(-40),
|
||||
memoryUpdates: ctx.memoryUpdates
|
||||
};
|
||||
}
|
||||
1
vibn-agent-runner/dist/prompts/atlas.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/prompts/atlas.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
221
vibn-agent-runner/dist/prompts/atlas.js
vendored
Normal file
221
vibn-agent-runner/dist/prompts/atlas.js
vendored
Normal file
@@ -0,0 +1,221 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const loader_1 = require("./loader");
|
||||
(0, loader_1.registerPrompt)('atlas', `
|
||||
You are Vibn, a senior product strategist and requirements architect. You guide product managers, founders, and non-technical stakeholders through the process of defining, scoping, and documenting a software product — from a rough idea to a comprehensive, implementation-ready Product Requirements Document (PRD).
|
||||
|
||||
You think like a principal PM at a top-tier product company: structured, pragmatic, user-obsessed, and cost-aware. You ask the right questions at the right time, challenge weak assumptions, and help people build the right thing — not just the thing they first described.
|
||||
|
||||
You never expose technical implementation details (databases, frameworks, hosting, APIs) unless the user explicitly asks. Your job is to help them think in terms of users, outcomes, features, and constraints — the platform handles the rest.
|
||||
|
||||
## Core Behavior Rules
|
||||
|
||||
1. **Lead with curiosity, not assumptions.** Never generate a full PRD from a single sentence. Conduct a structured discovery conversation first.
|
||||
2. **One phase at a time.** Move through the discovery phases sequentially. Don't skip ahead unless the user provides enough detail to justify it.
|
||||
3. **Summarize and keep moving.** At the end of each phase, briefly reflect back what you captured in 2–3 sentences, then immediately save the phase and continue to the next one. Do NOT ask "Does that sound right?" or wait for the user to confirm before advancing. If you're uncertain about something specific, note it as a quick question while still moving forward.
|
||||
4. **Challenge gently.** If the user's scope is too broad, their target audience is vague, or their feature list is a wishlist, push back constructively. Say things like: "That's a great long-term vision. For a strong v1, what's the one workflow that absolutely has to work on day one?"
|
||||
5. **Stay in product language.** Talk about users, journeys, features, screens, roles, permissions, integrations, and business rules — not tables, endpoints, or deployment pipelines.
|
||||
6. **Respect what you don't know.** If the user's domain is unfamiliar, ask clarifying questions rather than guessing. Incorrect assumptions in a PRD are expensive.
|
||||
7. **Be opinionated when it helps.** If the user is stuck, offer 2–3 concrete options with tradeoffs rather than open-ended questions. Guide, don't interrogate.
|
||||
|
||||
## Discovery Conversation Flow
|
||||
|
||||
Guide the user through these phases. You do NOT need to announce the phase names — just naturally move through the conversation. Adapt to what the user gives you; some users will dump a lot of context upfront, others will need to be drawn out.
|
||||
|
||||
### Phase 1 — The Big Picture
|
||||
Goal: Understand what they're building and why.
|
||||
- What is this product/tool/app in one sentence?
|
||||
- Who is it for? (Be specific — not "everyone" or "businesses")
|
||||
- What problem does it solve? What are people doing today instead?
|
||||
- What does success look like in 6 months?
|
||||
- Is this brand-new, a feature within something existing, or a replacement?
|
||||
- Are there competitors? What's different about this one?
|
||||
- Is there a hard deadline or external driver?
|
||||
|
||||
Output checkpoint: A concise problem statement and vision summary. Confirm with the user.
|
||||
|
||||
### Phase 2 — Users & Personas
|
||||
Goal: Define who uses this and what their experience looks like.
|
||||
- How many distinct types of users are there?
|
||||
- For each user type: what's their primary goal?
|
||||
- What does a "happy path" look like for each user type?
|
||||
- Are there permissions or access levels?
|
||||
- How do users sign up or get access?
|
||||
|
||||
Output checkpoint: A user persona summary with roles and primary workflows.
|
||||
|
||||
### Phase 3 — Feature Definition & Scope
|
||||
Goal: Define what the product actually does — and what it does NOT do.
|
||||
- Walk me through the core workflow step by step.
|
||||
- What are "must-have" features vs "nice-to-have"?
|
||||
- Any features from competitors you explicitly do NOT want?
|
||||
- Does this need to integrate with anything external?
|
||||
- Does this need to work on mobile, desktop, or both?
|
||||
- Any compliance or regulatory requirements?
|
||||
|
||||
Use MoSCoW when the feature list grows:
|
||||
- **Must have** — Product is broken without it
|
||||
- **Should have** — Important but can ship without for launch
|
||||
- **Could have** — Nice to have, adds polish
|
||||
- **Won't have (this version)** — Explicitly out of scope
|
||||
|
||||
Output checkpoint: A prioritized feature list with clear v1 boundary.
|
||||
|
||||
### Phase 4 — Business Model & Pricing
|
||||
Goal: Understand how this makes money and what the cost constraints are.
|
||||
- Is this revenue-generating or an internal tool?
|
||||
- If revenue: what's the pricing model?
|
||||
- Are there different tiers? What differentiates them?
|
||||
- Expected user volume at launch, 6 months, 12 months?
|
||||
- Budget ceiling for building and running this?
|
||||
- Third-party services with per-transaction costs?
|
||||
|
||||
Output checkpoint: Business model summary with pricing structure and cost considerations.
|
||||
|
||||
### Phase 5 — Content, Data & Key Screens
|
||||
Goal: Understand what users see and interact with.
|
||||
- What are the 5–8 most important screens or pages?
|
||||
- For each key screen: what's displayed? What actions can the user take?
|
||||
- Is there a dashboard? What's on it?
|
||||
- Are there notifications, emails, or alerts?
|
||||
- Does the product need search, filtering, sorting?
|
||||
- Any user-generated content?
|
||||
|
||||
Output checkpoint: A screen-by-screen overview of key interfaces.
|
||||
|
||||
### Phase 6 — Edge Cases, Risks & Open Questions
|
||||
Goal: Identify things that will cause problems later if not addressed now.
|
||||
- What happens when things go wrong?
|
||||
- Biggest risks to this project?
|
||||
- Assumptions that haven't been validated?
|
||||
- Legal, IP, or data ownership concerns?
|
||||
|
||||
Output checkpoint: A risk register and open questions list.
|
||||
|
||||
## PRD Generation
|
||||
|
||||
Once all phases are complete (or the user indicates they have enough), generate the final PRD using this structure:
|
||||
|
||||
\`\`\`
|
||||
# [Product Name] — Product Requirements Document
|
||||
|
||||
**Version:** 1.0
|
||||
**Status:** Draft
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
## 2. Problem Statement
|
||||
## 3. Vision & Success Metrics
|
||||
## 4. Target Users & Personas
|
||||
## 5. User Flows & Journeys
|
||||
## 6. Feature Requirements
|
||||
### 6.1 Must Have (v1 Launch)
|
||||
### 6.2 Should Have (Fast Follow)
|
||||
### 6.3 Could Have (Future)
|
||||
### 6.4 Explicitly Out of Scope
|
||||
## 7. Screen-by-Screen Specification
|
||||
## 8. Business Model & Pricing
|
||||
## 9. Integrations & External Dependencies
|
||||
## 10. Non-Functional Requirements
|
||||
## 11. Risks & Mitigations
|
||||
## 12. Open Questions & Assumptions
|
||||
## 13. Appendix
|
||||
\`\`\`
|
||||
|
||||
The PRD must include ALL sections 1–12 (skip 13. Appendix only if there is nothing to add). Do NOT truncate, summarise, or leave any section empty — write substantive content for every section based on what was discussed. If a section has limited information, write what you know and flag the gaps explicitly. Only call the finalize_prd tool once the entire document is written — do not call it mid-document.
|
||||
|
||||
## Conversation Style
|
||||
|
||||
- **Warm but efficient.** Don't waste time with filler. Every question should earn its place.
|
||||
- **Use concrete examples.** Instead of "What's your target audience?" say "Are we talking about solo freelancers managing 5 clients, or agency teams with 50+ accounts?"
|
||||
- **Mirror their language.** Match their vocabulary exactly.
|
||||
- **Celebrate progress.** Acknowledge when they clarify something well: "That's a clean distinction — that'll make the permissions model much simpler."
|
||||
- **Signal structure.** Let them know where they are: "Great, I've got a solid picture of your users. Let's talk about what they actually do in the product."
|
||||
- **Ask max 2–3 questions at a time.** Never overwhelm.
|
||||
|
||||
## Phase Checkpoints — Saving Progress
|
||||
|
||||
At the end of each phase, after you have summarised what you captured and the user has confirmed or added to it, append the following marker on its own line at the very end of your message. Do not include it mid-message or before you have confirmed the summary with the user.
|
||||
|
||||
Format (replace values, keep the exact tag):
|
||||
[[PHASE_COMPLETE:{"phase":"<phase_id>","title":"<Phase Title>","summary":"<1–2 sentence plain-English summary of what was captured>","data":{<key fields as a flat JSON object>}}]]
|
||||
|
||||
Phase IDs and their key data fields:
|
||||
- phase_id "big_picture" → fields: productName, problemStatement, targetUser, successMetric, competitors, deadline
|
||||
- phase_id "users_personas" → fields: userTypes (array), primaryGoals, accessModel, happyPath
|
||||
- phase_id "features_scope" → fields: mustHave (array), shouldHave (array), outOfScope (array), platforms, integrations
|
||||
- phase_id "business_model" → fields: revenueType, pricingModel, tiers (array), expectedVolume, budgetCeiling
|
||||
- phase_id "screens_data" → fields: keyScreens (array of {name, purpose, actions}), hasSearch, notifications
|
||||
- phase_id "risks_questions" → fields: risks (array), openQuestions (array), assumptions (array)
|
||||
|
||||
Rules:
|
||||
- Append the marker immediately after summarising the phase — do NOT wait for user confirmation first.
|
||||
- After appending the marker, immediately continue to the next phase question in the same message. Do not pause and wait for the user to respond before asking the next phase's questions.
|
||||
- Never guess — only include fields the user actually provided. Use null for unknown fields.
|
||||
- The marker will be hidden from the user and converted into a save indicator. Do not mention it.
|
||||
- Example: [[PHASE_COMPLETE:{"phase":"big_picture","title":"The Big Picture","summary":"Sportsy is a fantasy hockey management game inspired by OSM, targeting casual hockey fans aged 18–35.","data":{"productName":"Sportsy","problemStatement":"No compelling fantasy hockey management game exists for casual fans","targetUser":"Casual hockey fans 18–35","successMetric":"10k active users in 6 months","competitors":"OSM","deadline":null}}]]
|
||||
|
||||
## After the PRD Is Complete
|
||||
|
||||
When the \`finalize_prd\` tool call succeeds, send a closing message that:
|
||||
1. Acknowledges the PRD is saved
|
||||
2. Briefly explains what happens next — the platform will analyse the PRD and recommend a technical architecture (apps, services, infrastructure, integrations)
|
||||
3. Tells the user they can trigger that analysis right here in the chat when ready
|
||||
4. Appends the following marker on its own line at the very end of the message (nothing after it):
|
||||
|
||||
[[NEXT_STEP:{"action":"generate_architecture","label":"Analyse & generate architecture →"}]]
|
||||
|
||||
Keep the closing message warm and concise — 3–4 sentences max. Do not explain the architecture in detail; that's for the next step. Do not mention the marker.
|
||||
|
||||
Example closing message:
|
||||
"Your PRD for [Product Name] is complete and saved — great work getting all of that defined.
|
||||
|
||||
The next step is for the platform to read through everything you've outlined and recommend a technical architecture: the apps, services, and infrastructure your product will need. This takes about 30 seconds and you'll be able to review it before anything gets built.
|
||||
|
||||
Trigger the analysis whenever you're ready."
|
||||
|
||||
[[NEXT_STEP:{"action":"generate_architecture","label":"Analyse & generate architecture →"}]]
|
||||
|
||||
---
|
||||
|
||||
## Tools Available
|
||||
|
||||
You have access to a \`web_search\` tool. Use it when:
|
||||
- The user references a competitor, existing product, or market ("like Stripe", "similar to Notion", "OSM for hockey")
|
||||
- You need to verify what a product actually does before asking follow-up questions
|
||||
- The user's domain is unfamiliar and a quick search would help you ask better questions
|
||||
|
||||
Call it silently — don't announce you're searching. Just use the result to inform your next question or summary.
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Generating a full PRD from a one-line description
|
||||
- Asking more than 2–3 questions at once
|
||||
- Using technical jargon unless the user initiates it
|
||||
- Assuming features without confirmation
|
||||
- Treating every feature as must-have
|
||||
- Producing vague requirements ("The system should be fast")
|
||||
- Skipping the "out of scope" section
|
||||
- Ignoring business model questions
|
||||
|
||||
## Handling Edge Cases
|
||||
|
||||
- **User gives a massive brain dump:** Parse it, extract each phase's data, save all phases you have enough info for (one PHASE_COMPLETE marker per phase, each followed immediately by the next question or the next phase summary), then ask only about genuine gaps. Do not pause between phases for confirmation.
|
||||
- **User wants to skip straight to the PRD:** "I can generate a PRD right now, but the best PRDs come from about 10 minutes of focused conversation. The questions I'll ask will save weeks of rework later. Want to do a quick run-through?"
|
||||
- **User is vague:** Offer options — "Let me give you three common approaches and you tell me which feels closest…"
|
||||
- **User changes direction mid-conversation:** Acknowledge the pivot and resurface downstream impacts.
|
||||
- **User asks about technical implementation:** "Great question — the platform handles the technical architecture automatically based on what we define here. What matters for the PRD is [reframe to product question]."
|
||||
|
||||
## Opening Message
|
||||
|
||||
When you receive an internal init trigger to begin a new conversation (no prior history), introduce yourself naturally:
|
||||
|
||||
"Hey! I'm Vibn — I'm here to help you turn your product idea into a clear, detailed requirements document that's ready for implementation.
|
||||
|
||||
Whether you've got a rough concept or a detailed spec that needs tightening, I'll walk you through the key decisions and make sure nothing important falls through the cracks.
|
||||
|
||||
So — what are we building?"
|
||||
|
||||
Do not mention that you received an internal trigger. Just deliver the opening message naturally.
|
||||
`.trim());
|
||||
1
vibn-agent-runner/dist/prompts/coder.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/prompts/coder.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
31
vibn-agent-runner/dist/prompts/coder.js
vendored
Normal file
31
vibn-agent-runner/dist/prompts/coder.js
vendored
Normal 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());
|
||||
1
vibn-agent-runner/dist/prompts/import-analyzer.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/prompts/import-analyzer.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
98
vibn-agent-runner/dist/prompts/import-analyzer.js
vendored
Normal file
98
vibn-agent-runner/dist/prompts/import-analyzer.js
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const loader_1 = require("./loader");
|
||||
(0, loader_1.registerPrompt)('import-analyzer', `
|
||||
You are a senior software architect performing a codebase audit on a newly imported project.
|
||||
Your job is to thoroughly read the entire codebase, understand what it does and how it's built,
|
||||
then produce two documents: CODEBASE_MAP.md and MIGRATION_PLAN.md.
|
||||
|
||||
## Your goal
|
||||
|
||||
The founder who owns this project is non-technical. They need to understand what they have
|
||||
before deciding what to do with it. Write everything in plain language — no jargon, no
|
||||
assumptions that they know what "Docker" or "BigQuery" means without a brief explanation.
|
||||
|
||||
## Step 1 — Explore the full codebase
|
||||
|
||||
Use list_directory and find_files to map every folder and file.
|
||||
Use read_file to read key files:
|
||||
- README files (any depth)
|
||||
- package.json, requirements.txt, pyproject.toml (understand dependencies)
|
||||
- next.config.*, vite.config.*, Dockerfile, docker-compose.yml (understand deployment)
|
||||
- Any existing .md documentation
|
||||
- Main entry point files (index.ts, main.py, app.py, server.ts, etc.)
|
||||
- Environment variable files (.env.example — NEVER read actual .env files)
|
||||
|
||||
Do NOT read every file. Read enough to understand the purpose and structure of each component.
|
||||
|
||||
## Step 2 — Write CODEBASE_MAP.md
|
||||
|
||||
Create this file at the root of the repo. Structure it like this:
|
||||
|
||||
# Codebase Map
|
||||
|
||||
## What this project does
|
||||
[1–2 sentences in plain language explaining what the product is]
|
||||
|
||||
## Components
|
||||
|
||||
### [Component name] — [folder path]
|
||||
**Type:** [Web app / API server / Background job / AI agent / Scripts / etc.]
|
||||
**Language/Framework:** [e.g. Next.js 14 + TypeScript]
|
||||
**What it does:** [1–2 sentences plain language]
|
||||
**Status:** [Active / Incomplete / Legacy / Unknown]
|
||||
**Can deploy to Coolify:** [Yes / No / Maybe — with brief reason]
|
||||
|
||||
[repeat for each component]
|
||||
|
||||
## External Services Required
|
||||
[List every external service the project depends on, with a plain-language explanation of what it does]
|
||||
- **[Service name]**: [What it is, e.g. "Google BigQuery — stores all the analytics data"]
|
||||
|
||||
## Tech Stack Summary
|
||||
[Bullet list of languages and key frameworks]
|
||||
|
||||
## What's missing
|
||||
[Any obvious gaps: no tests, no CI, missing config files, etc.]
|
||||
|
||||
## Step 3 — Write MIGRATION_PLAN.md
|
||||
|
||||
Create this file at the root of the repo. Structure it like this:
|
||||
|
||||
# Migration Plan
|
||||
|
||||
## Summary
|
||||
[2–3 sentences: what's in good shape, what needs work, overall recommendation]
|
||||
|
||||
## Recommended Actions
|
||||
|
||||
### Deploy immediately (ready as-is)
|
||||
[List components that can be deployed to Coolify right now, with the folder path and any config notes]
|
||||
|
||||
### Keep on existing infrastructure
|
||||
[List components that should stay where they are and why — e.g. GCP Cloud Functions that depend on BigQuery]
|
||||
|
||||
### Migrate with work required
|
||||
[List components that could move to Coolify but need changes first]
|
||||
|
||||
### Archive or remove
|
||||
[Anything that looks abandoned, duplicate, or no longer needed]
|
||||
|
||||
## First steps
|
||||
[Numbered list of the 3–5 most important things to do, in order, written for a non-technical founder]
|
||||
|
||||
## Open questions
|
||||
[Things I couldn't determine from the code alone that the founder should clarify]
|
||||
|
||||
## Step 4 — Commit both files
|
||||
|
||||
Once both documents are written, commit them with:
|
||||
message: "docs: add CODEBASE_MAP and MIGRATION_PLAN from import analysis"
|
||||
|
||||
## Important rules
|
||||
- Never modify any existing files — only create the two new .md files
|
||||
- Never read .env files or files with credentials
|
||||
- Write for a non-technical founder — explain everything plainly
|
||||
- If you can't understand something, say so honestly in the document
|
||||
- Be specific: name actual files, folders, line counts, frameworks
|
||||
`.trim());
|
||||
7
vibn-agent-runner/dist/prompts/loader.d.ts
vendored
Normal file
7
vibn-agent-runner/dist/prompts/loader.d.ts
vendored
Normal 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
vibn-agent-runner/dist/prompts/loader.js
vendored
Normal file
30
vibn-agent-runner/dist/prompts/loader.js
vendored
Normal 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
vibn-agent-runner/dist/prompts/marketing.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/prompts/marketing.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
18
vibn-agent-runner/dist/prompts/marketing.js
vendored
Normal file
18
vibn-agent-runner/dist/prompts/marketing.js
vendored
Normal 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
vibn-agent-runner/dist/prompts/orchestrator.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/prompts/orchestrator.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
60
vibn-agent-runner/dist/prompts/orchestrator.js
vendored
Normal file
60
vibn-agent-runner/dist/prompts/orchestrator.js
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const loader_1 = require("./loader");
|
||||
(0, loader_1.registerPrompt)('orchestrator', `
|
||||
You are an AI executive assistant with full tool access to act on behalf of a software founder.
|
||||
|
||||
When project context is provided below, you are operating as the personal AI COO for that specific project — an executive partner to the founder. When no project context is provided, you operate as the Master Orchestrator for the Vibn platform itself.
|
||||
|
||||
## Platform context (always available)
|
||||
- Vibn frontend: vibnai.com (Next.js)
|
||||
- Agent runner: agents.vibnai.com (this system)
|
||||
- Self-hosted Git: git.vibnai.com (Gitea, user: mark)
|
||||
- Deployments: Coolify at coolify.vibnai.com (server: 34.19.250.135, Montreal)
|
||||
|
||||
## Your tools
|
||||
|
||||
**Awareness** (understand current state first):
|
||||
- list_repos — all Gitea repositories
|
||||
- list_all_issues — open/in-progress work items
|
||||
- list_all_apps — deployed apps and their status in Coolify
|
||||
- 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 a skill's full content
|
||||
|
||||
**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 facts across conversations
|
||||
|
||||
## Specialist agents you can spawn
|
||||
- **Coder** — writes code, tests, commits, pushes
|
||||
- **PM** — docs, issues, sprint tracking
|
||||
- **Marketing** — copy, release notes, blog posts
|
||||
|
||||
## How you work
|
||||
1. Use awareness tools first to understand the current state before acting.
|
||||
2. Break tasks into concrete steps.
|
||||
3. Before spawning an agent, call list_skills to check for relevant skills and pass them as context.
|
||||
4. Spawn the right agent(s) with specific, detailed instructions — never vague.
|
||||
5. Track and report results.
|
||||
6. Proactively surface issues: failed deploys, open bugs, stale work.
|
||||
7. Use save_memory to record decisions and facts you discover.
|
||||
|
||||
## Style
|
||||
- Direct. No filler. No "Great question!".
|
||||
- Honest about uncertainty — use tools to look things up rather than guessing.
|
||||
- When spawning agents, be specific — full context, not vague instructions.
|
||||
- Concise unless detail is needed.
|
||||
- Before delegating any significant work, state the scope in plain English and confirm.
|
||||
|
||||
## Security
|
||||
- Never spawn agents on: mark/vibn-frontend, mark/vibn-agent-runner, mark/vibn-api, mark/master-ai
|
||||
- Those are protected platform repos — read-only for awareness, never writable by agents.
|
||||
|
||||
{{knowledge}}
|
||||
`.trim());
|
||||
1
vibn-agent-runner/dist/prompts/pm.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/prompts/pm.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
20
vibn-agent-runner/dist/prompts/pm.js
vendored
Normal file
20
vibn-agent-runner/dist/prompts/pm.js
vendored
Normal 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());
|
||||
1
vibn-agent-runner/dist/server.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/server.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
610
vibn-agent-runner/dist/server.js
vendored
Normal file
610
vibn-agent-runner/dist/server.js
vendored
Normal file
@@ -0,0 +1,610 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = __importDefault(require("express"));
|
||||
const cors_1 = __importDefault(require("cors"));
|
||||
const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
const crypto = __importStar(require("crypto"));
|
||||
const child_process_1 = require("child_process");
|
||||
const job_store_1 = require("./job-store");
|
||||
const agent_runner_1 = require("./agent-runner");
|
||||
const agent_session_runner_1 = require("./agent-session-runner");
|
||||
const agents_1 = require("./agents");
|
||||
const security_1 = require("./tools/security");
|
||||
const orchestrator_1 = require("./orchestrator");
|
||||
const atlas_1 = require("./atlas");
|
||||
const llm_1 = require("./llm");
|
||||
const app = (0, express_1.default)();
|
||||
app.use((0, cors_1.default)());
|
||||
const startTime = new Date();
|
||||
// Raw body capture for webhook HMAC — must come before express.json()
|
||||
app.use('/webhook/gitea', express_1.default.raw({ type: '*/*' }));
|
||||
app.use(express_1.default.json());
|
||||
const PORT = process.env.PORT || 3333;
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build ToolContext from environment variables
|
||||
// ---------------------------------------------------------------------------
|
||||
function ensureWorkspace(repo) {
|
||||
const base = process.env.WORKSPACE_BASE || '/workspaces';
|
||||
if (!repo) {
|
||||
const dir = path.join(base, 'default');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
if (security_1.PROTECTED_GITEA_REPOS.has(repo)) {
|
||||
throw new Error(`SECURITY: Repo "${repo}" is a protected Vibn platform repo. ` +
|
||||
`Agents cannot clone or work in this workspace.`);
|
||||
}
|
||||
const dir = path.join(base, repo.replace('/', '_'));
|
||||
const gitea = {
|
||||
apiUrl: process.env.GITEA_API_URL || '',
|
||||
apiToken: process.env.GITEA_API_TOKEN || '',
|
||||
username: process.env.GITEA_USERNAME || ''
|
||||
};
|
||||
if (!fs.existsSync(path.join(dir, '.git'))) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const authedUrl = `${gitea.apiUrl}/${repo}.git`
|
||||
.replace('https://', `https://${gitea.username}:${gitea.apiToken}@`);
|
||||
try {
|
||||
(0, child_process_1.execSync)(`git clone "${authedUrl}" "${dir}"`, { stdio: 'pipe' });
|
||||
}
|
||||
catch {
|
||||
// Repo may not exist yet — just init
|
||||
(0, child_process_1.execSync)(`git init`, { cwd: dir, stdio: 'pipe' });
|
||||
(0, child_process_1.execSync)(`git remote add origin "${authedUrl}"`, { cwd: dir, stdio: 'pipe' });
|
||||
}
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
function buildContext(repo) {
|
||||
const workspaceRoot = ensureWorkspace(repo);
|
||||
return {
|
||||
workspaceRoot,
|
||||
gitea: {
|
||||
apiUrl: process.env.GITEA_API_URL || '',
|
||||
apiToken: process.env.GITEA_API_TOKEN || '',
|
||||
username: process.env.GITEA_USERNAME || ''
|
||||
},
|
||||
coolify: {
|
||||
apiUrl: process.env.COOLIFY_API_URL || '',
|
||||
apiToken: process.env.COOLIFY_API_TOKEN || ''
|
||||
},
|
||||
memoryUpdates: []
|
||||
};
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Routes
|
||||
// ---------------------------------------------------------------------------
|
||||
// Health check
|
||||
app.get('/health', (_req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// GitHub mirror — clone a public GitHub repo and push to Gitea as-is
|
||||
// ---------------------------------------------------------------------------
|
||||
app.post('/api/mirror', async (req, res) => {
|
||||
const { github_url, gitea_repo, project_name, github_token } = req.body;
|
||||
if (!github_url || !gitea_repo) {
|
||||
res.status(400).json({ error: '"github_url" and "gitea_repo" are required' });
|
||||
return;
|
||||
}
|
||||
const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
||||
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
||||
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
||||
const os = await Promise.resolve().then(() => __importStar(require('os')));
|
||||
const mirrorId = `mirror_${Date.now()}`;
|
||||
const tmpDir = path.join(os.tmpdir(), mirrorId);
|
||||
const gitea = {
|
||||
apiUrl: process.env.GITEA_API_URL || '',
|
||||
apiToken: process.env.GITEA_API_TOKEN || '',
|
||||
username: process.env.GITEA_USERNAME || ''
|
||||
};
|
||||
try {
|
||||
// Build authenticated Gitea push URL
|
||||
// GITEA_API_URL is like https://git.vibnai.com — strip /api/v1 if present
|
||||
const giteaBase = gitea.apiUrl.replace(/\/api\/v1\/?$/, '');
|
||||
const authedPushUrl = `${giteaBase}/${gitea_repo}.git`
|
||||
.replace('https://', `https://${gitea.username}:${gitea.apiToken}@`);
|
||||
console.log(`[mirror] Cloning ${github_url} → ${tmpDir}`);
|
||||
fs.mkdirSync(tmpDir, { recursive: true });
|
||||
// Build authenticated clone URL for private repos
|
||||
let cloneUrl = github_url;
|
||||
if (github_token) {
|
||||
cloneUrl = github_url.replace('https://', `https://${github_token}@`);
|
||||
}
|
||||
// Mirror-clone the GitHub repo (preserves all branches + tags)
|
||||
execSync(`git clone --mirror "${cloneUrl}" "${tmpDir}/.git"`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 120000
|
||||
});
|
||||
execSync(`git config --bool core.bare false`, { cwd: tmpDir, stdio: 'pipe' });
|
||||
execSync(`git checkout`, { cwd: tmpDir, stdio: 'pipe' });
|
||||
// Point origin at Gitea and push all refs
|
||||
execSync(`git remote set-url origin "${authedPushUrl}"`, { cwd: tmpDir, stdio: 'pipe' });
|
||||
execSync(`git push --mirror origin`, { cwd: tmpDir, stdio: 'pipe', timeout: 120000 });
|
||||
console.log(`[mirror] Pushed ${gitea_repo} successfully`);
|
||||
res.json({ success: true, gitea_repo, github_url });
|
||||
}
|
||||
catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[mirror] Failed:`, msg);
|
||||
res.status(500).json({ error: 'Mirror failed', details: msg });
|
||||
}
|
||||
finally {
|
||||
// Clean up temp dir
|
||||
try {
|
||||
const { execSync: rm } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
||||
rm(`rm -rf "${tmpDir}"`, { stdio: 'pipe' });
|
||||
}
|
||||
catch { /* best effort */ }
|
||||
}
|
||||
});
|
||||
// List available agents
|
||||
app.get('/api/agents', (_req, res) => {
|
||||
const agents = Object.values(agents_1.AGENTS).map(a => ({
|
||||
name: a.name,
|
||||
description: a.description,
|
||||
tools: a.tools.map(t => t.name)
|
||||
}));
|
||||
res.json(agents);
|
||||
});
|
||||
// Get server status and job statistics
|
||||
app.get('/api/status', (_req, res) => {
|
||||
const allJobs = (0, job_store_1.listJobs)(Infinity);
|
||||
const total_jobs = allJobs.length;
|
||||
const by_status = {
|
||||
queued: 0,
|
||||
running: 0,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
};
|
||||
for (const job of allJobs) {
|
||||
by_status[job.status] = (by_status[job.status] || 0) + 1;
|
||||
}
|
||||
const uptime_seconds = Math.floor((new Date().getTime() - startTime.getTime()) / 1000);
|
||||
const agents = Object.values(agents_1.AGENTS).map(a => a.name);
|
||||
res.json({
|
||||
total_jobs,
|
||||
by_status,
|
||||
uptime_seconds,
|
||||
agents,
|
||||
});
|
||||
});
|
||||
// Submit a new job
|
||||
app.post('/api/agent/run', async (req, res) => {
|
||||
const { agent: agentName, task, repo } = req.body;
|
||||
if (!agentName || !task) {
|
||||
res.status(400).json({ error: '"agent" and "task" are required' });
|
||||
return;
|
||||
}
|
||||
const agentConfig = agents_1.AGENTS[agentName];
|
||||
if (!agentConfig) {
|
||||
const available = Object.keys(agents_1.AGENTS).join(', ');
|
||||
res.status(400).json({ error: `Unknown agent "${agentName}". Available: ${available}` });
|
||||
return;
|
||||
}
|
||||
const job = (0, job_store_1.createJob)(agentName, task, repo);
|
||||
res.status(202).json({ jobId: job.id, status: job.status });
|
||||
// Run agent asynchronously
|
||||
const ctx = buildContext(repo);
|
||||
(0, agent_runner_1.runAgent)(job, agentConfig, task, ctx)
|
||||
.then(result => {
|
||||
(0, job_store_1.updateJob)(job.id, {
|
||||
status: 'completed',
|
||||
result: result.finalText,
|
||||
progress: `Done — ${result.turns} turns, ${result.toolCallCount} tool calls`
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
(0, job_store_1.updateJob)(job.id, {
|
||||
status: 'failed',
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
progress: 'Agent failed'
|
||||
});
|
||||
});
|
||||
});
|
||||
// Check job status
|
||||
app.get('/api/jobs/:id', (req, res) => {
|
||||
const job = (0, job_store_1.getJob)(req.params.id);
|
||||
if (!job) {
|
||||
res.status(404).json({ error: 'Job not found' });
|
||||
return;
|
||||
}
|
||||
res.json(job);
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// Orchestrator — persistent chat with full project context
|
||||
// ---------------------------------------------------------------------------
|
||||
app.post('/orchestrator/chat', async (req, res) => {
|
||||
const { message, session_id, history, knowledge_context } = req.body;
|
||||
if (!message) {
|
||||
res.status(400).json({ error: '"message" is required' });
|
||||
return;
|
||||
}
|
||||
const sessionId = session_id || `session_${Date.now()}`;
|
||||
const ctx = buildContext();
|
||||
try {
|
||||
const result = await (0, orchestrator_1.orchestratorChat)(sessionId, message, ctx, {
|
||||
preloadedHistory: history,
|
||||
knowledgeContext: knowledge_context
|
||||
});
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
||||
}
|
||||
});
|
||||
app.get('/orchestrator/sessions', (_req, res) => {
|
||||
res.json((0, orchestrator_1.listSessions)());
|
||||
});
|
||||
app.delete('/orchestrator/sessions/:id', (req, res) => {
|
||||
(0, orchestrator_1.clearSession)(req.params.id);
|
||||
res.json({ cleared: req.params.id });
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// Atlas — PRD discovery agent
|
||||
// ---------------------------------------------------------------------------
|
||||
app.post('/atlas/chat', async (req, res) => {
|
||||
const { message, session_id, history, is_init, } = req.body;
|
||||
if (!message) {
|
||||
res.status(400).json({ error: '"message" is required' });
|
||||
return;
|
||||
}
|
||||
const sessionId = session_id || `atlas_${Date.now()}`;
|
||||
const ctx = buildContext();
|
||||
try {
|
||||
const result = await (0, atlas_1.atlasChat)(sessionId, message, ctx, {
|
||||
preloadedHistory: history,
|
||||
isInit: is_init,
|
||||
});
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
||||
}
|
||||
});
|
||||
app.get('/atlas/sessions', (_req, res) => {
|
||||
res.json((0, atlas_1.listAtlasSessions)());
|
||||
});
|
||||
app.delete('/atlas/sessions/:id', (req, res) => {
|
||||
(0, atlas_1.clearAtlasSession)(req.params.id);
|
||||
res.json({ cleared: req.params.id });
|
||||
});
|
||||
// List recent jobs
|
||||
app.get('/api/jobs', (req, res) => {
|
||||
const limit = parseInt(req.query.limit || '20', 10);
|
||||
res.json((0, job_store_1.listJobs)(limit));
|
||||
});
|
||||
// Gitea webhook endpoint — triggers agent from an issue event
|
||||
app.post('/webhook/gitea', (req, res) => {
|
||||
const event = req.headers['x-gitea-event'];
|
||||
const rawBody = req.body;
|
||||
// Verify HMAC-SHA256 signature
|
||||
const webhookSecret = process.env.WEBHOOK_SECRET;
|
||||
if (webhookSecret) {
|
||||
const sig = req.headers['x-gitea-signature'];
|
||||
const expected = crypto
|
||||
.createHmac('sha256', webhookSecret)
|
||||
.update(rawBody)
|
||||
.digest('hex');
|
||||
if (!sig || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
|
||||
res.status(401).json({ error: 'Invalid webhook signature' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
const body = JSON.parse(rawBody.toString('utf8'));
|
||||
let task = null;
|
||||
let agentName = 'Coder';
|
||||
let repo;
|
||||
if (event === 'issues' && body.action === 'opened') {
|
||||
const issue = body.issue;
|
||||
repo = `${body.repository?.owner?.login}/${body.repository?.name}`;
|
||||
const labels = (issue.labels || []).map((l) => l.name);
|
||||
if (labels.includes('agent:pm')) {
|
||||
agentName = 'PM';
|
||||
}
|
||||
else if (labels.includes('agent:marketing')) {
|
||||
agentName = 'Marketing';
|
||||
}
|
||||
else if (labels.includes('agent:coder')) {
|
||||
agentName = 'Coder';
|
||||
}
|
||||
else {
|
||||
// No agent label — ignore
|
||||
res.json({ ignored: true, reason: 'no agent label on issue' });
|
||||
return;
|
||||
}
|
||||
task = `You have been assigned to resolve a Gitea issue in the repo ${repo}.\n\nIssue #${issue.number}: ${issue.title}\n\nDescription:\n${issue.body || '(no description)'}\n\nWhen done, close the issue by calling gitea_close_issue.`;
|
||||
}
|
||||
else if (event === 'push') {
|
||||
res.json({ ignored: true, reason: 'push events not auto-processed' });
|
||||
return;
|
||||
}
|
||||
else {
|
||||
res.json({ ignored: true, event });
|
||||
return;
|
||||
}
|
||||
if (!task) {
|
||||
res.json({ ignored: true });
|
||||
return;
|
||||
}
|
||||
const agentConfig = agents_1.AGENTS[agentName];
|
||||
const job = (0, job_store_1.createJob)(agentName, task, repo);
|
||||
res.status(202).json({ jobId: job.id, agent: agentName, event });
|
||||
const ctx = buildContext(repo);
|
||||
(0, agent_runner_1.runAgent)(job, agentConfig, task, ctx)
|
||||
.then(result => {
|
||||
(0, job_store_1.updateJob)(job.id, {
|
||||
status: 'completed',
|
||||
result: result.finalText,
|
||||
progress: `Done — ${result.turns} turns, ${result.toolCallCount} tool calls`
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
(0, job_store_1.updateJob)(job.id, {
|
||||
status: 'failed',
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
progress: 'Agent failed'
|
||||
});
|
||||
});
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent Execute — VIBN Build > Code > Agent tab
|
||||
//
|
||||
// Receives a task from the VIBN frontend, runs the Coder agent against
|
||||
// the project's Gitea repo, and streams progress back to the VIBN DB
|
||||
// via PATCH /api/projects/[id]/agent/sessions/[sid].
|
||||
//
|
||||
// This endpoint returns immediately (202) and runs the agent async so
|
||||
// the browser can close without killing the loop.
|
||||
// ---------------------------------------------------------------------------
|
||||
// Track active sessions for stop support
|
||||
const activeSessions = new Map();
|
||||
app.post('/agent/execute', async (req, res) => {
|
||||
const { sessionId, projectId, appName, appPath, giteaRepo, task, continueTask, autoApprove, coolifyAppUuid, } = req.body;
|
||||
if (!sessionId || !projectId || !appPath || !task) {
|
||||
res.status(400).json({ error: 'sessionId, projectId, appPath and task are required' });
|
||||
return;
|
||||
}
|
||||
const vibnApiUrl = process.env.VIBN_API_URL ?? 'https://vibnai.com';
|
||||
// Register session as active
|
||||
const sessionState = { stopped: false };
|
||||
activeSessions.set(sessionId, sessionState);
|
||||
// Respond immediately — execution is async
|
||||
res.status(202).json({ sessionId, status: 'running' });
|
||||
// Build workspace context — clone/update the Gitea repo if provided
|
||||
let ctx;
|
||||
try {
|
||||
ctx = buildContext(giteaRepo);
|
||||
}
|
||||
catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error('[agent/execute] buildContext failed:', msg);
|
||||
// Notify VIBN DB of failure
|
||||
fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'failed', error: msg }),
|
||||
}).catch(() => { });
|
||||
activeSessions.delete(sessionId);
|
||||
return;
|
||||
}
|
||||
// Capture repo root before scoping to appPath — needed for git commit in auto-approve
|
||||
const repoRoot = ctx.workspaceRoot;
|
||||
// Scope workspace to the app subdirectory so the agent works there naturally
|
||||
if (appPath) {
|
||||
const path = require('path');
|
||||
ctx.workspaceRoot = path.join(ctx.workspaceRoot, appPath);
|
||||
const fs = require('fs');
|
||||
fs.mkdirSync(ctx.workspaceRoot, { recursive: true });
|
||||
}
|
||||
const agentConfig = agents_1.AGENTS['Coder'];
|
||||
if (!agentConfig) {
|
||||
fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'failed', error: 'Coder agent not registered' }),
|
||||
}).catch(() => { });
|
||||
activeSessions.delete(sessionId);
|
||||
return;
|
||||
}
|
||||
// If continuing a previous task, combine into a single prompt so the agent
|
||||
// understands what was already attempted.
|
||||
const effectiveTask = continueTask
|
||||
? `Original task: ${task}\n\nFollow-up instruction: ${continueTask}`
|
||||
: task;
|
||||
// Run the streaming agent loop (fire and forget)
|
||||
(0, agent_session_runner_1.runSessionAgent)(agentConfig, effectiveTask, ctx, {
|
||||
sessionId,
|
||||
projectId,
|
||||
vibnApiUrl,
|
||||
appPath,
|
||||
repoRoot,
|
||||
isStopped: () => sessionState.stopped,
|
||||
autoApprove: autoApprove ?? true,
|
||||
giteaRepo,
|
||||
coolifyAppUuid,
|
||||
coolifyApiUrl: process.env.COOLIFY_API_URL,
|
||||
coolifyApiToken: process.env.COOLIFY_API_TOKEN,
|
||||
})
|
||||
.catch(err => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[agent/execute] session ${sessionId} crashed:`, msg);
|
||||
fetch(`${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'failed', error: msg }),
|
||||
}).catch(() => { });
|
||||
})
|
||||
.finally(() => {
|
||||
activeSessions.delete(sessionId);
|
||||
});
|
||||
});
|
||||
app.post('/agent/stop', (req, res) => {
|
||||
const { sessionId } = req.body;
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ error: 'sessionId required' });
|
||||
return;
|
||||
}
|
||||
const session = activeSessions.get(sessionId);
|
||||
if (session) {
|
||||
session.stopped = true;
|
||||
res.json({ ok: true, message: 'Stop signal sent — agent will halt after current step.' });
|
||||
}
|
||||
else {
|
||||
res.json({ ok: true, message: 'Session not active (may have already completed).' });
|
||||
}
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent Approve — commit and push agent's changes to Gitea, trigger deploy
|
||||
//
|
||||
// Called by vibn-frontend after the user reviews changed files and clicks
|
||||
// "Approve & commit". The agent runner does git add/commit/push in the
|
||||
// workspace where the agent was working.
|
||||
// ---------------------------------------------------------------------------
|
||||
app.post('/agent/approve', async (req, res) => {
|
||||
const { giteaRepo, commitMessage, coolifyApiUrl, coolifyApiToken, coolifyAppUuid } = req.body;
|
||||
if (!giteaRepo || !commitMessage) {
|
||||
res.status(400).json({ error: 'giteaRepo and commitMessage are required' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Resolve the workspace root for this repo (does NOT re-clone if already present)
|
||||
const workspaceRoot = ensureWorkspace(giteaRepo);
|
||||
// Configure git identity for this commit
|
||||
const gitea = {
|
||||
username: process.env.GITEA_USERNAME || 'agent',
|
||||
apiToken: process.env.GITEA_API_TOKEN || '',
|
||||
apiUrl: process.env.GITEA_API_URL || '',
|
||||
};
|
||||
const { execSync: exec } = require('child_process');
|
||||
const gitOpts = { cwd: workspaceRoot, stdio: 'pipe' };
|
||||
// Ensure git identity
|
||||
try {
|
||||
exec('git config user.email "agent@vibnai.com"', gitOpts);
|
||||
exec('git config user.name "VIBN Agent"', gitOpts);
|
||||
}
|
||||
catch { /* already set */ }
|
||||
// Stage all changes
|
||||
exec('git add -A', gitOpts);
|
||||
// Check if there is anything to commit
|
||||
let status;
|
||||
try {
|
||||
status = exec('git status --porcelain', gitOpts).toString().trim();
|
||||
}
|
||||
catch {
|
||||
status = '';
|
||||
}
|
||||
if (!status) {
|
||||
res.json({ ok: true, committed: false, message: 'Nothing to commit — working tree is clean.' });
|
||||
return;
|
||||
}
|
||||
// Commit
|
||||
exec(`git commit -m ${JSON.stringify(commitMessage)}`, gitOpts);
|
||||
// Push — use token auth embedded in remote URL
|
||||
const authedUrl = `${gitea.apiUrl}/${giteaRepo}.git`
|
||||
.replace('https://', `https://${gitea.username}:${gitea.apiToken}@`);
|
||||
exec(`git push "${authedUrl}" HEAD:main`, gitOpts);
|
||||
// Optionally trigger a Coolify redeploy
|
||||
let deployed = false;
|
||||
if (coolifyApiUrl && coolifyApiToken && coolifyAppUuid) {
|
||||
try {
|
||||
const deployRes = await fetch(`${coolifyApiUrl}/api/v1/applications/${coolifyAppUuid}/start`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${coolifyApiToken}` },
|
||||
});
|
||||
deployed = deployRes.ok;
|
||||
}
|
||||
catch { /* deploy trigger is best-effort */ }
|
||||
}
|
||||
res.json({ ok: true, committed: true, deployed, message: `Committed and pushed: "${commitMessage}"` });
|
||||
}
|
||||
catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error('[agent/approve]', msg);
|
||||
res.status(500).json({ error: msg });
|
||||
}
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generate — thin structured-generation endpoint (no session, no system prompt)
|
||||
// Use this for one-shot tasks like architecture recommendations.
|
||||
// ---------------------------------------------------------------------------
|
||||
app.post('/generate', async (req, res) => {
|
||||
const { prompt, model, region } = req.body;
|
||||
if (!prompt) {
|
||||
res.status(400).json({ error: '"prompt" is required' });
|
||||
return;
|
||||
}
|
||||
// Allow overriding CLAUDE_REGION per-request for testing
|
||||
const prevRegion = process.env.CLAUDE_REGION;
|
||||
if (region)
|
||||
process.env.CLAUDE_REGION = region;
|
||||
try {
|
||||
const llm = (0, llm_1.createLLM)(model ?? 'A', { temperature: 0.3 });
|
||||
const messages = [
|
||||
{ role: 'user', content: prompt }
|
||||
];
|
||||
const response = await llm.chat(messages, [], 8192);
|
||||
res.json({ reply: response.content ?? '', model: llm.modelId });
|
||||
}
|
||||
catch (err) {
|
||||
res.status(500).json({ error: err instanceof Error ? err.message : String(err), model });
|
||||
}
|
||||
finally {
|
||||
if (region)
|
||||
process.env.CLAUDE_REGION = prevRegion ?? '';
|
||||
}
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error handler
|
||||
// ---------------------------------------------------------------------------
|
||||
app.use((err, _req, res, _next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).json({ error: err.message });
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// Start
|
||||
// ---------------------------------------------------------------------------
|
||||
app.listen(PORT, () => {
|
||||
console.log(`AgentRunner listening on port ${PORT}`);
|
||||
console.log(`Agents available: ${Object.keys(agents_1.AGENTS).join(', ')}`);
|
||||
if (!process.env.GOOGLE_API_KEY) {
|
||||
console.warn('WARNING: GOOGLE_API_KEY is not set — agents will fail');
|
||||
}
|
||||
});
|
||||
1
vibn-agent-runner/dist/test.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/test.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
13
vibn-agent-runner/dist/test.js
vendored
Normal file
13
vibn-agent-runner/dist/test.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const assert_1 = __importDefault(require("assert"));
|
||||
function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
assert_1.default.strictEqual(add(1, 2), 3, 'add(1, 2) should be 3');
|
||||
assert_1.default.strictEqual(add(0, 0), 0, 'add(0, 0) should be 0');
|
||||
assert_1.default.strictEqual(add(-1, 1), 0, 'add(-1, 1) should be 0');
|
||||
console.log('All tests passed!');
|
||||
10
vibn-agent-runner/dist/tools/agent-api.d.ts
vendored
Normal file
10
vibn-agent-runner/dist/tools/agent-api.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface AgentRunnerConfig {
|
||||
runnerUrl: string;
|
||||
}
|
||||
export interface SpawnAgentInput {
|
||||
agent: string;
|
||||
task: string;
|
||||
repo: string;
|
||||
}
|
||||
export declare function spawnAgent(cfg: AgentRunnerConfig, input: SpawnAgentInput): Promise<unknown>;
|
||||
export declare function getJobStatus(cfg: AgentRunnerConfig, jobId: string): Promise<unknown>;
|
||||
40
vibn-agent-runner/dist/tools/agent-api.js
vendored
Normal file
40
vibn-agent-runner/dist/tools/agent-api.js
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Pure sub-agent orchestration API. Wraps the vibn-agent-runner HTTP endpoints
|
||||
// so the same logic is usable from the in-process tool and from an MCP server.
|
||||
// =============================================================================
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.spawnAgent = spawnAgent;
|
||||
exports.getJobStatus = getJobStatus;
|
||||
async function spawnAgent(cfg, input) {
|
||||
try {
|
||||
const res = await fetch(`${cfg.runnerUrl}/api/agent/run`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Internal': 'true' },
|
||||
body: JSON.stringify({ agent: input.agent, task: input.task, repo: input.repo }),
|
||||
});
|
||||
const data = (await res.json());
|
||||
return { jobId: data.jobId, agent: input.agent, status: 'dispatched' };
|
||||
}
|
||||
catch (err) {
|
||||
return { error: `Failed to spawn agent: ${err instanceof Error ? err.message : String(err)}` };
|
||||
}
|
||||
}
|
||||
async function getJobStatus(cfg, jobId) {
|
||||
try {
|
||||
const res = await fetch(`${cfg.runnerUrl}/api/jobs/${jobId}`);
|
||||
const job = (await res.json());
|
||||
return {
|
||||
id: job.id,
|
||||
agent: job.agent,
|
||||
status: job.status,
|
||||
progress: job.progress,
|
||||
toolCalls: job.toolCalls?.length,
|
||||
result: job.result,
|
||||
error: job.error,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
return { error: `Failed to get job: ${err instanceof Error ? err.message : String(err)}` };
|
||||
}
|
||||
}
|
||||
1
vibn-agent-runner/dist/tools/agent.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/tools/agent.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
73
vibn-agent-runner/dist/tools/agent.js
vendored
Normal file
73
vibn-agent-runner/dist/tools/agent.js
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Sub-agent orchestration tool registrations. Logic lives in ./agent-api.ts.
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const registry_1 = require("./registry");
|
||||
const api = __importStar(require("./agent-api"));
|
||||
function runnerUrl() {
|
||||
return process.env.AGENT_RUNNER_URL || 'http://localhost:3333';
|
||||
}
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'spawn_agent',
|
||||
description: 'Dispatch a sub-agent job to run in the background. Returns a job ID. Use this to delegate specialized work to Coder, PM, or Marketing agents.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
agent: { type: 'string', description: '"Coder", "PM", or "Marketing"' },
|
||||
task: { type: 'string', description: 'Detailed task description for the agent' },
|
||||
repo: { type: 'string', description: 'Gitea repo in "owner/name" format the agent should work on' }
|
||||
},
|
||||
required: ['agent', 'task', 'repo']
|
||||
},
|
||||
async handler(args, _ctx) {
|
||||
return api.spawnAgent({ runnerUrl: runnerUrl() }, { agent: String(args.agent), task: String(args.task), repo: String(args.repo) });
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'get_job_status',
|
||||
description: 'Check the status of a previously spawned agent job by job ID.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: { type: 'string', description: 'Job ID returned by spawn_agent' }
|
||||
},
|
||||
required: ['job_id']
|
||||
},
|
||||
async handler(args, _ctx) {
|
||||
return api.getJobStatus({ runnerUrl: runnerUrl() }, String(args.job_id));
|
||||
}
|
||||
});
|
||||
19
vibn-agent-runner/dist/tools/context.d.ts
vendored
Normal file
19
vibn-agent-runner/dist/tools/context.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface MemoryUpdate {
|
||||
key: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
export interface ToolContext {
|
||||
workspaceRoot: string;
|
||||
gitea: {
|
||||
apiUrl: string;
|
||||
apiToken: string;
|
||||
username: string;
|
||||
};
|
||||
coolify: {
|
||||
apiUrl: string;
|
||||
apiToken: string;
|
||||
};
|
||||
/** Accumulated memory updates from save_memory tool calls in this turn */
|
||||
memoryUpdates: MemoryUpdate[];
|
||||
}
|
||||
2
vibn-agent-runner/dist/tools/context.js
vendored
Normal file
2
vibn-agent-runner/dist/tools/context.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
11
vibn-agent-runner/dist/tools/coolify-api.d.ts
vendored
Normal file
11
vibn-agent-runner/dist/tools/coolify-api.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface CoolifyConfig {
|
||||
apiUrl: string;
|
||||
apiToken: string;
|
||||
}
|
||||
export declare function listProjects(cfg: CoolifyConfig): Promise<unknown>;
|
||||
export declare function listApplications(cfg: CoolifyConfig, projectUuid: string): Promise<unknown>;
|
||||
export declare function deploy(cfg: CoolifyConfig, applicationUuid: string): Promise<unknown>;
|
||||
export declare function getLogs(cfg: CoolifyConfig, applicationUuid: string, limit?: number): Promise<unknown>;
|
||||
export declare function listAllApps(cfg: CoolifyConfig): Promise<unknown>;
|
||||
export declare function getAppStatus(cfg: CoolifyConfig, appName: string): Promise<unknown>;
|
||||
export declare function deployApp(cfg: CoolifyConfig, appName: string): Promise<unknown>;
|
||||
118
vibn-agent-runner/dist/tools/coolify-api.js
vendored
Normal file
118
vibn-agent-runner/dist/tools/coolify-api.js
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Pure Coolify API — no ToolContext coupling, no registry coupling.
|
||||
//
|
||||
// Everything in here takes a plain { apiUrl, apiToken } config and calls
|
||||
// the Coolify v1 API directly. Security guardrails (PROTECTED_COOLIFY_PROJECT,
|
||||
// PROTECTED_COOLIFY_APPS, assertCoolifyDeployable) are enforced inside each
|
||||
// function so every caller — in-process tool handler, MCP server, or future
|
||||
// direct SDK user — gets the same protection.
|
||||
//
|
||||
// This is the shared core consumed by:
|
||||
// - tools/coolify.ts (in-process registry used by agent-runner loop)
|
||||
// - mcp/coolify-server.ts (stdio MCP server exposed to Goose/Claude/Cursor)
|
||||
// =============================================================================
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.listProjects = listProjects;
|
||||
exports.listApplications = listApplications;
|
||||
exports.deploy = deploy;
|
||||
exports.getLogs = getLogs;
|
||||
exports.listAllApps = listAllApps;
|
||||
exports.getAppStatus = getAppStatus;
|
||||
exports.deployApp = deployApp;
|
||||
const security_1 = require("./security");
|
||||
async function coolifyFetch(cfg, path, method = 'GET', body) {
|
||||
const res = await fetch(`${cfg.apiUrl}/api/v1${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${cfg.apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
});
|
||||
if (!res.ok) {
|
||||
return { error: `Coolify API error: ${res.status} ${res.statusText}` };
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API — each function corresponds 1:1 with a registered tool today
|
||||
// ---------------------------------------------------------------------------
|
||||
async function listProjects(cfg) {
|
||||
const projects = await coolifyFetch(cfg, '/projects');
|
||||
if (!Array.isArray(projects))
|
||||
return projects;
|
||||
return projects.filter((p) => p.uuid !== security_1.PROTECTED_COOLIFY_PROJECT);
|
||||
}
|
||||
async function listApplications(cfg, projectUuid) {
|
||||
const all = await coolifyFetch(cfg, '/applications');
|
||||
if (!Array.isArray(all))
|
||||
return all;
|
||||
return all.filter((a) => a.project_uuid === projectUuid);
|
||||
}
|
||||
async function deploy(cfg, applicationUuid) {
|
||||
(0, security_1.assertCoolifyDeployable)(applicationUuid);
|
||||
const apps = await coolifyFetch(cfg, '/applications');
|
||||
if (Array.isArray(apps)) {
|
||||
const app = apps.find((a) => a.uuid === applicationUuid);
|
||||
if (app?.project_uuid === security_1.PROTECTED_COOLIFY_PROJECT) {
|
||||
return {
|
||||
error: `SECURITY: App "${applicationUuid}" belongs to the protected Vibn project. Agents cannot deploy platform apps.`
|
||||
};
|
||||
}
|
||||
}
|
||||
return coolifyFetch(cfg, `/applications/${applicationUuid}/deploy`, 'POST');
|
||||
}
|
||||
async function getLogs(cfg, applicationUuid, limit = 50) {
|
||||
return coolifyFetch(cfg, `/applications/${applicationUuid}/logs?limit=${limit}`);
|
||||
}
|
||||
async function listAllApps(cfg) {
|
||||
const apps = await coolifyFetch(cfg, '/applications');
|
||||
if (!Array.isArray(apps))
|
||||
return apps;
|
||||
return apps
|
||||
.filter((a) => a.project_uuid !== security_1.PROTECTED_COOLIFY_PROJECT && !security_1.PROTECTED_COOLIFY_APPS.has(a.uuid))
|
||||
.map((a) => ({
|
||||
uuid: a.uuid,
|
||||
name: a.name,
|
||||
fqdn: a.fqdn,
|
||||
status: a.status,
|
||||
repo: a.git_repository,
|
||||
branch: a.git_branch
|
||||
}));
|
||||
}
|
||||
async function getAppStatus(cfg, appName) {
|
||||
const apps = await coolifyFetch(cfg, '/applications');
|
||||
if (!Array.isArray(apps))
|
||||
return apps;
|
||||
const app = apps.find((a) => a.name?.toLowerCase() === appName.toLowerCase() || a.uuid === appName);
|
||||
if (!app)
|
||||
return { error: `App "${appName}" not found` };
|
||||
if (security_1.PROTECTED_COOLIFY_APPS.has(app.uuid) || app.project_uuid === security_1.PROTECTED_COOLIFY_PROJECT) {
|
||||
return {
|
||||
error: `SECURITY: "${appName}" is a protected Vibn platform app. Status is not exposed to agents.`
|
||||
};
|
||||
}
|
||||
const logs = await coolifyFetch(cfg, `/applications/${app.uuid}/logs?limit=20`);
|
||||
return { name: app.name, uuid: app.uuid, status: app.status, fqdn: app.fqdn, logs };
|
||||
}
|
||||
async function deployApp(cfg, appName) {
|
||||
const apps = await coolifyFetch(cfg, '/applications');
|
||||
if (!Array.isArray(apps))
|
||||
return apps;
|
||||
const app = apps.find((a) => a.name?.toLowerCase() === appName.toLowerCase() || a.uuid === appName);
|
||||
if (!app)
|
||||
return { error: `App "${appName}" not found` };
|
||||
if (security_1.PROTECTED_COOLIFY_APPS.has(app.uuid) || app.project_uuid === security_1.PROTECTED_COOLIFY_PROJECT) {
|
||||
return {
|
||||
error: `SECURITY: "${appName}" is a protected Vibn platform application. ` +
|
||||
`Agents can only deploy user project apps, not platform infrastructure.`
|
||||
};
|
||||
}
|
||||
// Non-project-prefixed deploy endpoint — older Coolify entry point still in use
|
||||
const result = await fetch(`${cfg.apiUrl}/api/v1/deploy?uuid=${app.uuid}&force=false`, {
|
||||
headers: { 'Authorization': `Bearer ${cfg.apiToken}` }
|
||||
});
|
||||
return result.json();
|
||||
}
|
||||
1
vibn-agent-runner/dist/tools/coolify.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/tools/coolify.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
131
vibn-agent-runner/dist/tools/coolify.js
vendored
Normal file
131
vibn-agent-runner/dist/tools/coolify.js
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Coolify tool registrations (in-process path used by agent-runner).
|
||||
//
|
||||
// All logic lives in ./coolify-api.ts so the MCP server (src/mcp/coolify-server.ts)
|
||||
// and this in-process registry call the exact same code path. Keep this file
|
||||
// purely about: (a) surface-shape for the LLM (name/description/parameters),
|
||||
// (b) mapping ctx.coolify → CoolifyConfig. No business logic here.
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const registry_1 = require("./registry");
|
||||
const api = __importStar(require("./coolify-api"));
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'coolify_list_projects',
|
||||
description: 'List all projects in the Coolify instance. Returns project names and UUIDs.',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
async handler(_args, ctx) {
|
||||
return api.listProjects(ctx.coolify);
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'coolify_list_applications',
|
||||
description: 'List applications in a Coolify project.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project_uuid: { type: 'string', description: 'Project UUID from coolify_list_projects' }
|
||||
},
|
||||
required: ['project_uuid']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.listApplications(ctx.coolify, String(args.project_uuid));
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'coolify_deploy',
|
||||
description: 'Trigger a deployment for a Coolify application.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
application_uuid: { type: 'string', description: 'Application UUID to deploy' }
|
||||
},
|
||||
required: ['application_uuid']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.deploy(ctx.coolify, String(args.application_uuid));
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'coolify_get_logs',
|
||||
description: 'Get recent deployment logs for a Coolify application.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
application_uuid: { type: 'string', description: 'Application UUID' }
|
||||
},
|
||||
required: ['application_uuid']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.getLogs(ctx.coolify, String(args.application_uuid));
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'list_all_apps',
|
||||
description: 'List all Coolify applications across all projects with their status (running/stopped/error) and domain.',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
async handler(_args, ctx) {
|
||||
return api.listAllApps(ctx.coolify);
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'get_app_status',
|
||||
description: 'Get the current deployment status and recent logs for a specific Coolify application by name or UUID.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
app_name: { type: 'string', description: 'Application name (e.g. "vibn-frontend") or UUID' }
|
||||
},
|
||||
required: ['app_name']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.getAppStatus(ctx.coolify, String(args.app_name));
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'deploy_app',
|
||||
description: 'Trigger a Coolify deployment for an app by name. Use after an agent commits code.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
app_name: { type: 'string', description: 'Application name (e.g. "vibn-frontend")' }
|
||||
},
|
||||
required: ['app_name']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.deployApp(ctx.coolify, String(args.app_name));
|
||||
}
|
||||
});
|
||||
6
vibn-agent-runner/dist/tools/file-api.d.ts
vendored
Normal file
6
vibn-agent-runner/dist/tools/file-api.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export declare function readFile(workspaceRoot: string, relPath: string): Promise<unknown>;
|
||||
export declare function writeFile(workspaceRoot: string, relPath: string, content: string): Promise<unknown>;
|
||||
export declare function replaceInFile(workspaceRoot: string, relPath: string, oldContent: string, newContent: string): Promise<unknown>;
|
||||
export declare function listDirectory(workspaceRoot: string, relPath: string): Promise<unknown>;
|
||||
export declare function findFiles(workspaceRoot: string, pattern: string): Promise<unknown>;
|
||||
export declare function searchCode(workspaceRoot: string, query: string, fileExtensions?: string[]): Promise<unknown>;
|
||||
149
vibn-agent-runner/dist/tools/file-api.js
vendored
Normal file
149
vibn-agent-runner/dist/tools/file-api.js
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Pure file-system API — no ToolContext coupling.
|
||||
// Takes a workspaceRoot string and safely-resolves paths beneath it.
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.readFile = readFile;
|
||||
exports.writeFile = writeFile;
|
||||
exports.replaceInFile = replaceInFile;
|
||||
exports.listDirectory = listDirectory;
|
||||
exports.findFiles = findFiles;
|
||||
exports.searchCode = searchCode;
|
||||
const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
const cp = __importStar(require("child_process"));
|
||||
const util = __importStar(require("util"));
|
||||
const minimatch_1 = require("minimatch");
|
||||
const utils_1 = require("./utils");
|
||||
const execAsync = util.promisify(cp.exec);
|
||||
async function readFile(workspaceRoot, relPath) {
|
||||
const abs = (0, utils_1.safeResolve)(workspaceRoot, relPath);
|
||||
try {
|
||||
return fs.readFileSync(abs, 'utf8');
|
||||
}
|
||||
catch {
|
||||
return { error: `File not found: ${relPath}` };
|
||||
}
|
||||
}
|
||||
async function writeFile(workspaceRoot, relPath, content) {
|
||||
const abs = (0, utils_1.safeResolve)(workspaceRoot, relPath);
|
||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||
fs.writeFileSync(abs, content, 'utf8');
|
||||
return { success: true, path: relPath, bytes: Buffer.byteLength(content) };
|
||||
}
|
||||
async function replaceInFile(workspaceRoot, relPath, oldContent, newContent) {
|
||||
const abs = (0, utils_1.safeResolve)(workspaceRoot, relPath);
|
||||
const current = fs.readFileSync(abs, 'utf8');
|
||||
if (!current.includes(oldContent)) {
|
||||
return { error: 'old_content not found in file. Read the file again to get the current content.' };
|
||||
}
|
||||
fs.writeFileSync(abs, current.replace(oldContent, newContent), 'utf8');
|
||||
return { success: true, path: relPath };
|
||||
}
|
||||
async function listDirectory(workspaceRoot, relPath) {
|
||||
const abs = (0, utils_1.safeResolve)(workspaceRoot, relPath);
|
||||
try {
|
||||
const entries = fs.readdirSync(abs, { withFileTypes: true });
|
||||
return entries
|
||||
.filter(e => !utils_1.EXCLUDED.has(e.name))
|
||||
.map(e => e.isDirectory() ? `${e.name}/` : e.name);
|
||||
}
|
||||
catch {
|
||||
return { error: `Directory not found: ${relPath}` };
|
||||
}
|
||||
}
|
||||
async function findFiles(workspaceRoot, pattern) {
|
||||
const matcher = new minimatch_1.Minimatch(pattern, { dot: false });
|
||||
const results = [];
|
||||
function walk(dir) {
|
||||
if (results.length >= 200)
|
||||
return;
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
}
|
||||
catch {
|
||||
return;
|
||||
}
|
||||
for (const e of entries) {
|
||||
if (utils_1.EXCLUDED.has(e.name))
|
||||
continue;
|
||||
const abs = path.join(dir, e.name);
|
||||
const rel = path.relative(workspaceRoot, abs).split(path.sep).join('/');
|
||||
if (e.isDirectory()) {
|
||||
walk(abs);
|
||||
}
|
||||
else if (matcher.match(rel)) {
|
||||
results.push(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(workspaceRoot);
|
||||
return { files: results, truncated: results.length >= 200 };
|
||||
}
|
||||
async function searchCode(workspaceRoot, query, fileExtensions) {
|
||||
const globPatterns = fileExtensions?.map(e => `*.${e}`) || [];
|
||||
const rgArgs = ['--line-number', '--no-heading', '--color=never', '--max-count=30'];
|
||||
for (const ex of utils_1.EXCLUDED) {
|
||||
rgArgs.push('--glob', `!${ex}`);
|
||||
}
|
||||
if (globPatterns.length > 0) {
|
||||
for (const g of globPatterns)
|
||||
rgArgs.push('--glob', g);
|
||||
}
|
||||
rgArgs.push('--fixed-strings', query, workspaceRoot);
|
||||
try {
|
||||
const { stdout } = await execAsync(`rg ${rgArgs.map(a => `'${a}'`).join(' ')}`, {
|
||||
cwd: workspaceRoot, timeout: 15000,
|
||||
});
|
||||
return stdout.trim().split('\n').filter(Boolean).map(line => {
|
||||
const m = line.match(/^(.+?):(\d+):(.*)$/);
|
||||
if (!m)
|
||||
return null;
|
||||
return {
|
||||
file: path.relative(workspaceRoot, m[1]).split(path.sep).join('/'),
|
||||
line: parseInt(m[2]),
|
||||
content: m[3].trim(),
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}
|
||||
catch (err) {
|
||||
if (err.code === 1)
|
||||
return []; // ripgrep exit 1 = no matches
|
||||
return { error: `Search failed: ${err.message}` };
|
||||
}
|
||||
}
|
||||
1
vibn-agent-runner/dist/tools/file.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/tools/file.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
134
vibn-agent-runner/dist/tools/file.js
vendored
Normal file
134
vibn-agent-runner/dist/tools/file.js
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// File-system tool registrations (in-process path used by agent-runner).
|
||||
// All logic lives in ./file-api.ts.
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const registry_1 = require("./registry");
|
||||
const api = __importStar(require("./file-api"));
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'read_file',
|
||||
description: 'Read the complete content of a file in the workspace. Always read before editing.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Relative path from workspace root (e.g. "src/index.ts")' }
|
||||
},
|
||||
required: ['path']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.readFile(ctx.workspaceRoot, String(args.path));
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'write_file',
|
||||
description: 'Write complete content to a file. Creates parent directories if needed. Overwrites existing files.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Relative path from workspace root' },
|
||||
content: { type: 'string', description: 'Complete new file content' }
|
||||
},
|
||||
required: ['path', 'content']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.writeFile(ctx.workspaceRoot, String(args.path), String(args.content));
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'replace_in_file',
|
||||
description: 'Replace an exact string in a file. The old_content must match character-for-character. Read the file first.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Relative path from workspace root' },
|
||||
old_content: { type: 'string', description: 'Exact text to replace' },
|
||||
new_content: { type: 'string', description: 'Replacement text' }
|
||||
},
|
||||
required: ['path', 'old_content', 'new_content']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.replaceInFile(ctx.workspaceRoot, String(args.path), String(args.old_content), String(args.new_content));
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'list_directory',
|
||||
description: 'List files and subdirectories in a directory. Directories have trailing "/".',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Relative path from workspace root. Use "." for root.' }
|
||||
},
|
||||
required: ['path']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.listDirectory(ctx.workspaceRoot, String(args.path));
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'find_files',
|
||||
description: 'Find files matching a glob pattern in the workspace. Returns up to 200 relative paths.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
pattern: { type: 'string', description: 'Glob pattern e.g. "**/*.ts", "src/**/*.test.js"' }
|
||||
},
|
||||
required: ['pattern']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.findFiles(ctx.workspaceRoot, String(args.pattern));
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'search_code',
|
||||
description: 'Search file contents for a string or regex pattern. Returns file path, line number, and matching line.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Search term or regex' },
|
||||
file_extensions: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Optional: limit to these extensions e.g. ["ts","js"]'
|
||||
}
|
||||
},
|
||||
required: ['query']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
const exts = Array.isArray(args.file_extensions) ? args.file_extensions : undefined;
|
||||
return api.searchCode(ctx.workspaceRoot, String(args.query), exts);
|
||||
}
|
||||
});
|
||||
6
vibn-agent-runner/dist/tools/git-api.d.ts
vendored
Normal file
6
vibn-agent-runner/dist/tools/git-api.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface GitPushConfig {
|
||||
apiUrl: string;
|
||||
apiToken: string;
|
||||
username: string;
|
||||
}
|
||||
export declare function gitCommitAndPush(workspaceRoot: string, message: string, cfg: GitPushConfig): Promise<unknown>;
|
||||
86
vibn-agent-runner/dist/tools/git-api.js
vendored
Normal file
86
vibn-agent-runner/dist/tools/git-api.js
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Pure git API — no ToolContext coupling.
|
||||
// Requires a GitPushConfig with Gitea credentials for authenticated push.
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.gitCommitAndPush = gitCommitAndPush;
|
||||
const cp = __importStar(require("child_process"));
|
||||
const util = __importStar(require("util"));
|
||||
const security_1 = require("./security");
|
||||
const execAsync = util.promisify(cp.exec);
|
||||
async function gitCommitAndPush(workspaceRoot, message, cfg) {
|
||||
const cwd = workspaceRoot;
|
||||
const { apiUrl, apiToken, username } = cfg;
|
||||
try {
|
||||
// Check remote URL before committing — block pushes to protected repos
|
||||
let remoteCheck = '';
|
||||
try {
|
||||
remoteCheck = (await execAsync('git remote get-url origin', { cwd })).stdout.trim();
|
||||
}
|
||||
catch { /* no remote yet */ }
|
||||
for (const protectedRepo of security_1.PROTECTED_GITEA_REPOS) {
|
||||
const repoPath = protectedRepo.replace('mark/', '');
|
||||
if (remoteCheck.includes(`/${repoPath}`) || remoteCheck.includes(`/${repoPath}.git`)) {
|
||||
return {
|
||||
error: `SECURITY: This workspace is linked to a protected Vibn platform repo (${protectedRepo}). ` +
|
||||
`Agents cannot push to platform repos. Only user project repos are writable.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
await execAsync('git add -A', { cwd });
|
||||
await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd });
|
||||
// Strip any existing credentials from remote URL and re-inject cleanly
|
||||
let remoteUrl = '';
|
||||
try {
|
||||
remoteUrl = (await execAsync('git remote get-url origin', { cwd })).stdout.trim();
|
||||
}
|
||||
catch { /* no remote */ }
|
||||
const cleanUrl = remoteUrl.replace(/https:\/\/[^@]+@/, 'https://');
|
||||
const baseUrl = cleanUrl || apiUrl;
|
||||
const authedUrl = baseUrl.replace('https://', `https://${username}:${apiToken}@`);
|
||||
await execAsync(`git remote set-url origin "${authedUrl}"`, { cwd }).catch(async () => {
|
||||
await execAsync(`git remote add origin "${authedUrl}"`, { cwd });
|
||||
});
|
||||
const branch = (await execAsync('git rev-parse --abbrev-ref HEAD', { cwd })).stdout.trim();
|
||||
await execAsync(`git push -u origin "${branch}"`, { cwd, timeout: 60000 });
|
||||
return { success: true, message, branch };
|
||||
}
|
||||
catch (err) {
|
||||
const cleaned = (err.message || '').replace(new RegExp(apiToken, 'g'), '***');
|
||||
return { error: `Git operation failed: ${cleaned}` };
|
||||
}
|
||||
}
|
||||
1
vibn-agent-runner/dist/tools/git.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/tools/git.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
58
vibn-agent-runner/dist/tools/git.js
vendored
Normal file
58
vibn-agent-runner/dist/tools/git.js
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Git commit-and-push tool registration. Logic lives in ./git-api.ts.
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const registry_1 = require("./registry");
|
||||
const api = __importStar(require("./git-api"));
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'git_commit_and_push',
|
||||
description: 'Stage all changes, commit with a message, and push to the remote. Call this when work is complete.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: { type: 'string', description: 'Commit message describing the changes made' }
|
||||
},
|
||||
required: ['message']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.gitCommitAndPush(ctx.workspaceRoot, String(args.message), {
|
||||
apiUrl: ctx.gitea.apiUrl,
|
||||
apiToken: ctx.gitea.apiToken,
|
||||
username: ctx.gitea.username,
|
||||
});
|
||||
}
|
||||
});
|
||||
20
vibn-agent-runner/dist/tools/gitea-api.d.ts
vendored
Normal file
20
vibn-agent-runner/dist/tools/gitea-api.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface GiteaConfig {
|
||||
apiUrl: string;
|
||||
apiToken: string;
|
||||
username?: string;
|
||||
}
|
||||
export interface CreateIssueInput {
|
||||
repo: string;
|
||||
title: string;
|
||||
body: string;
|
||||
labels?: string[];
|
||||
}
|
||||
export declare function createIssue(cfg: GiteaConfig, input: CreateIssueInput): Promise<unknown>;
|
||||
export declare function listIssues(cfg: GiteaConfig, repo: string, state?: string): Promise<unknown>;
|
||||
export declare function closeIssue(cfg: GiteaConfig, repo: string, issueNumber: number): Promise<unknown>;
|
||||
export declare function listRepos(cfg: GiteaConfig): Promise<unknown>;
|
||||
export declare function listAllIssues(cfg: GiteaConfig, opts?: {
|
||||
repo?: string;
|
||||
state?: string;
|
||||
}): Promise<unknown>;
|
||||
export declare function readRepoFile(cfg: GiteaConfig, repo: string, filePath: string): Promise<unknown>;
|
||||
121
vibn-agent-runner/dist/tools/gitea-api.js
vendored
Normal file
121
vibn-agent-runner/dist/tools/gitea-api.js
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Pure Gitea API — no ToolContext coupling, no registry coupling.
|
||||
//
|
||||
// Takes a plain { apiUrl, apiToken, username } config. Security guardrails
|
||||
// (PROTECTED_GITEA_REPOS, assertGiteaWritable) are enforced inside each
|
||||
// function so every caller gets the same protection.
|
||||
//
|
||||
// Consumed by:
|
||||
// - tools/gitea.ts (in-process registry used by agent-runner loop)
|
||||
// - mcp/gitea-server.ts (stdio MCP server exposed to any MCP client)
|
||||
// =============================================================================
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createIssue = createIssue;
|
||||
exports.listIssues = listIssues;
|
||||
exports.closeIssue = closeIssue;
|
||||
exports.listRepos = listRepos;
|
||||
exports.listAllIssues = listAllIssues;
|
||||
exports.readRepoFile = readRepoFile;
|
||||
const security_1 = require("./security");
|
||||
async function giteaFetch(cfg, path, method = 'GET', body) {
|
||||
const res = await fetch(`${cfg.apiUrl}/api/v1${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
'Authorization': `token ${cfg.apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
});
|
||||
if (!res.ok) {
|
||||
return { error: `Gitea API error: ${res.status} ${res.statusText}` };
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
async function createIssue(cfg, input) {
|
||||
(0, security_1.assertGiteaWritable)(input.repo);
|
||||
return giteaFetch(cfg, `/repos/${input.repo}/issues`, 'POST', {
|
||||
title: input.title,
|
||||
body: input.body,
|
||||
labels: input.labels,
|
||||
});
|
||||
}
|
||||
async function listIssues(cfg, repo, state = 'open') {
|
||||
return giteaFetch(cfg, `/repos/${repo}/issues?state=${state}&limit=20`);
|
||||
}
|
||||
async function closeIssue(cfg, repo, issueNumber) {
|
||||
(0, security_1.assertGiteaWritable)(repo);
|
||||
return giteaFetch(cfg, `/repos/${repo}/issues/${issueNumber}`, 'PATCH', { state: 'closed' });
|
||||
}
|
||||
async function listRepos(cfg) {
|
||||
const res = await fetch(`${cfg.apiUrl}/api/v1/repos/search?limit=50`, {
|
||||
headers: { 'Authorization': `token ${cfg.apiToken}` }
|
||||
});
|
||||
if (!res.ok) {
|
||||
return { error: `Gitea API error: ${res.status} ${res.statusText}` };
|
||||
}
|
||||
const data = await res.json();
|
||||
return (data.data || [])
|
||||
.filter((r) => !security_1.PROTECTED_GITEA_REPOS.has(r.full_name))
|
||||
.map((r) => ({
|
||||
name: r.full_name,
|
||||
description: r.description,
|
||||
default_branch: r.default_branch,
|
||||
updated: r.updated,
|
||||
stars: r.stars_count,
|
||||
open_issues: r.open_issues_count,
|
||||
}));
|
||||
}
|
||||
async function listAllIssues(cfg, opts = {}) {
|
||||
const state = opts.state || 'open';
|
||||
if (opts.repo) {
|
||||
if (security_1.PROTECTED_GITEA_REPOS.has(opts.repo)) {
|
||||
return {
|
||||
error: `SECURITY: "${opts.repo}" is a protected Vibn platform repo. Agents cannot access its issues.`
|
||||
};
|
||||
}
|
||||
return giteaFetch(cfg, `/repos/${opts.repo}/issues?state=${state}&limit=20`);
|
||||
}
|
||||
// Fetch across all non-protected repos (cap at 10 repos to bound request count)
|
||||
const reposRes = await fetch(`${cfg.apiUrl}/api/v1/repos/search?limit=50`, {
|
||||
headers: { 'Authorization': `token ${cfg.apiToken}` }
|
||||
});
|
||||
if (!reposRes.ok) {
|
||||
return { error: `Gitea API error: ${reposRes.status} ${reposRes.statusText}` };
|
||||
}
|
||||
const reposData = await reposRes.json();
|
||||
const repos = (reposData.data || []).filter((r) => !security_1.PROTECTED_GITEA_REPOS.has(r.full_name));
|
||||
const allIssues = [];
|
||||
for (const r of repos.slice(0, 10)) {
|
||||
const issues = await giteaFetch(cfg, `/repos/${r.full_name}/issues?state=${state}&limit=10`);
|
||||
if (Array.isArray(issues)) {
|
||||
allIssues.push(...issues.map((i) => ({
|
||||
repo: r.full_name,
|
||||
number: i.number,
|
||||
title: i.title,
|
||||
state: i.state,
|
||||
labels: i.labels?.map((l) => l.name),
|
||||
created: i.created_at,
|
||||
})));
|
||||
}
|
||||
}
|
||||
return allIssues;
|
||||
}
|
||||
async function readRepoFile(cfg, repo, filePath) {
|
||||
try {
|
||||
const res = await fetch(`${cfg.apiUrl}/api/v1/repos/${repo}/contents/${filePath}`, {
|
||||
headers: { 'Authorization': `token ${cfg.apiToken}` }
|
||||
});
|
||||
if (!res.ok)
|
||||
return { error: `File not found: ${filePath} in ${repo}` };
|
||||
const data = await res.json();
|
||||
const content = Buffer.from(data.content, 'base64').toString('utf8');
|
||||
return { repo, path: filePath, content };
|
||||
}
|
||||
catch (err) {
|
||||
return {
|
||||
error: `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
||||
};
|
||||
}
|
||||
}
|
||||
1
vibn-agent-runner/dist/tools/gitea.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/tools/gitea.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
137
vibn-agent-runner/dist/tools/gitea.js
vendored
Normal file
137
vibn-agent-runner/dist/tools/gitea.js
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Gitea tool registrations (in-process path used by agent-runner).
|
||||
//
|
||||
// All logic lives in ./gitea-api.ts so the MCP server (src/mcp/gitea-server.ts)
|
||||
// and this in-process registry call the exact same code path. Keep this file
|
||||
// purely about: (a) surface-shape for the LLM (name/description/parameters),
|
||||
// (b) mapping ctx.gitea → GiteaConfig. No business logic here.
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const registry_1 = require("./registry");
|
||||
const api = __importStar(require("./gitea-api"));
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'gitea_create_issue',
|
||||
description: 'Create a new issue in a Gitea repository.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repo: { type: 'string', description: 'Repository in "owner/name" format' },
|
||||
title: { type: 'string', description: 'Issue title' },
|
||||
body: { type: 'string', description: 'Issue body (markdown)' },
|
||||
labels: { type: 'array', items: { type: 'string' }, description: 'Optional label names' }
|
||||
},
|
||||
required: ['repo', 'title', 'body']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.createIssue(ctx.gitea, {
|
||||
repo: String(args.repo),
|
||||
title: String(args.title),
|
||||
body: String(args.body),
|
||||
labels: Array.isArray(args.labels) ? args.labels : undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'gitea_list_issues',
|
||||
description: 'List open issues in a Gitea repository.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repo: { type: 'string', description: 'Repository in "owner/name" format' },
|
||||
state: { type: 'string', description: '"open", "closed", or "all". Default: "open"' }
|
||||
},
|
||||
required: ['repo']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.listIssues(ctx.gitea, String(args.repo), String(args.state || 'open'));
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'gitea_close_issue',
|
||||
description: 'Close an issue in a Gitea repository.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repo: { type: 'string', description: 'Repository in "owner/name" format' },
|
||||
issue_number: { type: 'number', description: 'Issue number to close' }
|
||||
},
|
||||
required: ['repo', 'issue_number']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.closeIssue(ctx.gitea, String(args.repo), Number(args.issue_number));
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'list_repos',
|
||||
description: 'List all Git repositories in the Gitea organization. Returns repo names, descriptions, and last update time.',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
async handler(_args, ctx) {
|
||||
return api.listRepos(ctx.gitea);
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'list_all_issues',
|
||||
description: 'List open issues across all repos or a specific repo. Use this to understand what work is queued or in progress.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repo: { type: 'string', description: 'Optional: "owner/name" to scope to one repo. Omit for all repos.' },
|
||||
state: { type: 'string', description: '"open", "closed", or "all". Default: "open"' }
|
||||
}
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.listAllIssues(ctx.gitea, {
|
||||
repo: args.repo ? String(args.repo) : undefined,
|
||||
state: args.state ? String(args.state) : undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'read_repo_file',
|
||||
description: 'Read a file from any Gitea repository without cloning it. Useful for understanding project structure.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repo: { type: 'string', description: 'Repo in "owner/name" format' },
|
||||
path: { type: 'string', description: 'File path within the repo (e.g. "src/app/page.tsx")' }
|
||||
},
|
||||
required: ['repo', 'path']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
return api.readRepoFile(ctx.gitea, String(args.repo), String(args.path));
|
||||
}
|
||||
});
|
||||
13
vibn-agent-runner/dist/tools/index.d.ts
vendored
Normal file
13
vibn-agent-runner/dist/tools/index.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import './file';
|
||||
import './shell';
|
||||
import './git';
|
||||
import './gitea';
|
||||
import './coolify';
|
||||
import './agent';
|
||||
import './memory';
|
||||
import './skills';
|
||||
import './prd';
|
||||
import './search';
|
||||
export { ALL_TOOLS, executeTool, ToolDefinition } from './registry';
|
||||
export { ToolContext, MemoryUpdate } from './context';
|
||||
export { PROTECTED_GITEA_REPOS, PROTECTED_COOLIFY_PROJECT, PROTECTED_COOLIFY_APPS, assertGiteaWritable, assertCoolifyDeployable } from './security';
|
||||
25
vibn-agent-runner/dist/tools/index.js
vendored
Normal file
25
vibn-agent-runner/dist/tools/index.js
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.assertCoolifyDeployable = exports.assertGiteaWritable = exports.PROTECTED_COOLIFY_APPS = exports.PROTECTED_COOLIFY_PROJECT = exports.PROTECTED_GITEA_REPOS = exports.executeTool = exports.ALL_TOOLS = void 0;
|
||||
// Import domain files first — side effects register each tool into the registry.
|
||||
// Order determines ALL_TOOLS array order (informational only).
|
||||
require("./file");
|
||||
require("./shell");
|
||||
require("./git");
|
||||
require("./gitea");
|
||||
require("./coolify");
|
||||
require("./agent");
|
||||
require("./memory");
|
||||
require("./skills");
|
||||
require("./prd");
|
||||
require("./search");
|
||||
// Re-export the public API — identical surface to the old tools.ts
|
||||
var registry_1 = require("./registry");
|
||||
Object.defineProperty(exports, "ALL_TOOLS", { enumerable: true, get: function () { return registry_1.ALL_TOOLS; } });
|
||||
Object.defineProperty(exports, "executeTool", { enumerable: true, get: function () { return registry_1.executeTool; } });
|
||||
var security_1 = require("./security");
|
||||
Object.defineProperty(exports, "PROTECTED_GITEA_REPOS", { enumerable: true, get: function () { return security_1.PROTECTED_GITEA_REPOS; } });
|
||||
Object.defineProperty(exports, "PROTECTED_COOLIFY_PROJECT", { enumerable: true, get: function () { return security_1.PROTECTED_COOLIFY_PROJECT; } });
|
||||
Object.defineProperty(exports, "PROTECTED_COOLIFY_APPS", { enumerable: true, get: function () { return security_1.PROTECTED_COOLIFY_APPS; } });
|
||||
Object.defineProperty(exports, "assertGiteaWritable", { enumerable: true, get: function () { return security_1.assertGiteaWritable; } });
|
||||
Object.defineProperty(exports, "assertCoolifyDeployable", { enumerable: true, get: function () { return security_1.assertCoolifyDeployable; } });
|
||||
17
vibn-agent-runner/dist/tools/memory-api.d.ts
vendored
Normal file
17
vibn-agent-runner/dist/tools/memory-api.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface MemoryEntry {
|
||||
key: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
export interface MemoryInput {
|
||||
key: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
export declare function toEntry(input: MemoryInput): MemoryEntry;
|
||||
export declare function saveMemoryToStore(sessionKey: string, input: MemoryInput): {
|
||||
saved: true;
|
||||
entry: MemoryEntry;
|
||||
};
|
||||
export declare function listMemoryFromStore(sessionKey: string): MemoryEntry[];
|
||||
export declare function clearMemoryStore(sessionKey: string): void;
|
||||
33
vibn-agent-runner/dist/tools/memory-api.js
vendored
Normal file
33
vibn-agent-runner/dist/tools/memory-api.js
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Pure memory API. The in-process agent-runner collects memory updates into an
|
||||
// array on the ToolContext (ctx.memoryUpdates) so the supervisor loop can
|
||||
// persist them at end-of-turn. MCP clients don't share that array, so the MCP
|
||||
// server keeps its own module-level store keyed by an optional sessionKey.
|
||||
// =============================================================================
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.toEntry = toEntry;
|
||||
exports.saveMemoryToStore = saveMemoryToStore;
|
||||
exports.listMemoryFromStore = listMemoryFromStore;
|
||||
exports.clearMemoryStore = clearMemoryStore;
|
||||
function toEntry(input) {
|
||||
return { key: input.key, type: input.type, value: input.value };
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
// In-memory store used by the MCP server path (the in-process path
|
||||
// appends directly to ctx.memoryUpdates and ignores this store).
|
||||
// -------------------------------------------------------------------
|
||||
const memoryStore = new Map();
|
||||
function saveMemoryToStore(sessionKey, input) {
|
||||
const entry = toEntry(input);
|
||||
const list = memoryStore.get(sessionKey) ?? [];
|
||||
list.push(entry);
|
||||
memoryStore.set(sessionKey, list);
|
||||
return { saved: true, entry };
|
||||
}
|
||||
function listMemoryFromStore(sessionKey) {
|
||||
return [...(memoryStore.get(sessionKey) ?? [])];
|
||||
}
|
||||
function clearMemoryStore(sessionKey) {
|
||||
memoryStore.delete(sessionKey);
|
||||
}
|
||||
1
vibn-agent-runner/dist/tools/memory.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/tools/memory.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
68
vibn-agent-runner/dist/tools/memory.js
vendored
Normal file
68
vibn-agent-runner/dist/tools/memory.js
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// save_memory tool registration. Logic lives in ./memory-api.ts.
|
||||
// In-process: appends to ctx.memoryUpdates so the supervisor loop can persist
|
||||
// at end-of-turn. MCP server path uses memory-api's internal store.
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const registry_1 = require("./registry");
|
||||
const api = __importStar(require("./memory-api"));
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'save_memory',
|
||||
description: 'Persist an important fact about this project to long-term memory. Use this to save decisions, tech stack choices, feature descriptions, constraints, or goals so they are remembered across conversations.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string', description: 'Short unique label (e.g. "primary_language", "auth_strategy", "deploy_target")' },
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['tech_stack', 'decision', 'feature', 'goal', 'constraint', 'note'],
|
||||
description: 'Category of the memory item'
|
||||
},
|
||||
value: { type: 'string', description: 'The fact to remember (1-3 sentences)' }
|
||||
},
|
||||
required: ['key', 'type', 'value']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
const entry = api.toEntry({
|
||||
key: String(args.key),
|
||||
type: String(args.type),
|
||||
value: String(args.value),
|
||||
});
|
||||
ctx.memoryUpdates.push(entry);
|
||||
return { saved: true, key: entry.key, type: entry.type };
|
||||
}
|
||||
});
|
||||
7
vibn-agent-runner/dist/tools/prd-api.d.ts
vendored
Normal file
7
vibn-agent-runner/dist/tools/prd-api.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/** sessionKey (workspaceRoot) → PRD markdown */
|
||||
export declare const prdStore: Map<string, string>;
|
||||
export declare function finalizePrd(sessionKey: string, content: string): {
|
||||
saved: true;
|
||||
message: string;
|
||||
};
|
||||
export declare function getPrd(sessionKey: string): string | null;
|
||||
22
vibn-agent-runner/dist/tools/prd-api.js
vendored
Normal file
22
vibn-agent-runner/dist/tools/prd-api.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Pure PRD API. The store is module-level so atlas.ts can inspect it after each
|
||||
// turn (it imports `prdStore` from prd.ts which re-exports from here). Keep
|
||||
// this module side-effect-free otherwise.
|
||||
// =============================================================================
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.prdStore = void 0;
|
||||
exports.finalizePrd = finalizePrd;
|
||||
exports.getPrd = getPrd;
|
||||
/** sessionKey (workspaceRoot) → PRD markdown */
|
||||
exports.prdStore = new Map();
|
||||
function finalizePrd(sessionKey, content) {
|
||||
exports.prdStore.set(sessionKey, content);
|
||||
return {
|
||||
saved: true,
|
||||
message: 'PRD saved. Let the user know their product requirements document is ready and the platform will now architect the technical solution.',
|
||||
};
|
||||
}
|
||||
function getPrd(sessionKey) {
|
||||
return exports.prdStore.get(sessionKey) ?? null;
|
||||
}
|
||||
1
vibn-agent-runner/dist/tools/prd.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/tools/prd.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export { prdStore } from './prd-api';
|
||||
62
vibn-agent-runner/dist/tools/prd.js
vendored
Normal file
62
vibn-agent-runner/dist/tools/prd.js
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// finalize_prd tool registration. Logic + store live in ./prd-api.ts.
|
||||
// We re-export `prdStore` so existing imports (atlas.ts) continue to work.
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.prdStore = void 0;
|
||||
const registry_1 = require("./registry");
|
||||
const api = __importStar(require("./prd-api"));
|
||||
var prd_api_1 = require("./prd-api");
|
||||
Object.defineProperty(exports, "prdStore", { enumerable: true, get: function () { return prd_api_1.prdStore; } });
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'finalize_prd',
|
||||
description: 'Call this when you have finished writing the complete PRD document. Pass the full PRD markdown as content. This saves the document and signals to the user that discovery is complete.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'The complete PRD document in markdown format'
|
||||
}
|
||||
},
|
||||
required: ['content']
|
||||
},
|
||||
async handler(args, ctx) {
|
||||
// Store against workspaceRoot as a unique key (each project has its own workspace)
|
||||
return api.finalizePrd(ctx.workspaceRoot, String(args.content));
|
||||
}
|
||||
});
|
||||
16
vibn-agent-runner/dist/tools/registry.d.ts
vendored
Normal file
16
vibn-agent-runner/dist/tools/registry.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ToolContext } from './context';
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, unknown>;
|
||||
/** Implementation — called by executeTool(). Not sent to the LLM. */
|
||||
handler: (args: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
|
||||
}
|
||||
/**
|
||||
* Mutable array kept in sync with the registry.
|
||||
* Used by agents.ts to pick tool subsets by name (backwards-compatible with ALL_TOOLS).
|
||||
*/
|
||||
export declare const ALL_TOOLS: ToolDefinition[];
|
||||
export declare function registerTool(tool: ToolDefinition): void;
|
||||
/** Dispatch a tool call by name — O(1) map lookup, no switch needed. */
|
||||
export declare function executeTool(name: string, args: Record<string, unknown>, ctx: ToolContext): Promise<unknown>;
|
||||
23
vibn-agent-runner/dist/tools/registry.js
vendored
Normal file
23
vibn-agent-runner/dist/tools/registry.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ALL_TOOLS = void 0;
|
||||
exports.registerTool = registerTool;
|
||||
exports.executeTool = executeTool;
|
||||
/** Live registry — grows as domain files are imported. */
|
||||
const _registry = new Map();
|
||||
/**
|
||||
* Mutable array kept in sync with the registry.
|
||||
* Used by agents.ts to pick tool subsets by name (backwards-compatible with ALL_TOOLS).
|
||||
*/
|
||||
exports.ALL_TOOLS = [];
|
||||
function registerTool(tool) {
|
||||
_registry.set(tool.name, tool);
|
||||
exports.ALL_TOOLS.push(tool);
|
||||
}
|
||||
/** Dispatch a tool call by name — O(1) map lookup, no switch needed. */
|
||||
async function executeTool(name, args, ctx) {
|
||||
const tool = _registry.get(name);
|
||||
if (!tool)
|
||||
return { error: `Unknown tool: ${name}` };
|
||||
return tool.handler(args, ctx);
|
||||
}
|
||||
1
vibn-agent-runner/dist/tools/search-api.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/tools/search-api.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export declare function webSearch(query: string): Promise<unknown>;
|
||||
54
vibn-agent-runner/dist/tools/search-api.js
vendored
Normal file
54
vibn-agent-runner/dist/tools/search-api.js
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// Pure web-search API via DuckDuckGo HTML endpoint. No API key required.
|
||||
// =============================================================================
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.webSearch = webSearch;
|
||||
async function webSearch(query) {
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed)
|
||||
return { error: 'No query provided' };
|
||||
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(trimmed)}`;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; VIBN-Atlas/1.0)',
|
||||
Accept: 'text/html',
|
||||
},
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!res.ok) {
|
||||
return { error: `Search failed with status ${res.status}` };
|
||||
}
|
||||
const html = await res.text();
|
||||
const titles = [];
|
||||
for (const m of html.matchAll(/class="result__a"[^>]*href="[^"]*"[^>]*>(.*?)<\/a>/gs)) {
|
||||
const title = m[1].replace(/<[^>]+>/g, '').trim();
|
||||
if (title)
|
||||
titles.push(title);
|
||||
}
|
||||
const snippets = [];
|
||||
for (const m of html.matchAll(/class="result__snippet"[^>]*>(.*?)<\/a>/gs)) {
|
||||
const snippet = m[1].replace(/<[^>]+>/g, '').trim();
|
||||
if (snippet)
|
||||
snippets.push(snippet);
|
||||
}
|
||||
const count = Math.min(6, Math.max(titles.length, snippets.length));
|
||||
const results = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const title = titles[i] || '';
|
||||
const snippet = snippets[i] || '';
|
||||
if (title || snippet)
|
||||
results.push(`**${title}**\n${snippet}`);
|
||||
}
|
||||
if (results.length === 0)
|
||||
return { error: 'No results found' };
|
||||
const text = results.join('\n\n');
|
||||
const truncated = text.length > 5000 ? text.slice(0, 5000) + '\n\n[...results truncated]' : text;
|
||||
return { query: trimmed, results: truncated };
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { error: `Search request failed: ${message}` };
|
||||
}
|
||||
}
|
||||
1
vibn-agent-runner/dist/tools/search.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/tools/search.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
57
vibn-agent-runner/dist/tools/search.js
vendored
Normal file
57
vibn-agent-runner/dist/tools/search.js
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// web_search tool registration. Logic lives in ./search-api.ts.
|
||||
// =============================================================================
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const registry_1 = require("./registry");
|
||||
const api = __importStar(require("./search-api"));
|
||||
(0, registry_1.registerTool)({
|
||||
name: 'web_search',
|
||||
description: 'Search the web for current information. Use this to research competitors, market trends, pricing models, existing solutions, technology choices, or any topic the user mentions that would benefit from real-world context. Returns a summary of top search results.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'The search query. Be specific — e.g. "SaaS project management tools pricing 2024" rather than just "project management".'
|
||||
}
|
||||
},
|
||||
required: ['query']
|
||||
},
|
||||
async handler(args) {
|
||||
return api.webSearch(String(args.query));
|
||||
}
|
||||
});
|
||||
11
vibn-agent-runner/dist/tools/security.d.ts
vendored
Normal file
11
vibn-agent-runner/dist/tools/security.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/** Gitea repos agents can NEVER push to, commit to, or write issues on. */
|
||||
export declare const PROTECTED_GITEA_REPOS: Set<string>;
|
||||
/** Coolify project UUID for the VIBN platform — agents cannot deploy here. */
|
||||
export declare const PROTECTED_COOLIFY_PROJECT = "f4owwggokksgw0ogo0844os0";
|
||||
/**
|
||||
* Specific Coolify app UUIDs that must never be deployed by an agent.
|
||||
* Belt-and-suspenders check in case the project UUID filter is bypassed.
|
||||
*/
|
||||
export declare const PROTECTED_COOLIFY_APPS: Set<string>;
|
||||
export declare function assertGiteaWritable(repo: string): void;
|
||||
export declare function assertCoolifyDeployable(appUuid: string): void;
|
||||
43
vibn-agent-runner/dist/tools/security.js
vendored
Normal file
43
vibn-agent-runner/dist/tools/security.js
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
// =============================================================================
|
||||
// SECURITY GUARDRAILS — Protected VIBN Platform Resources
|
||||
//
|
||||
// These repos and Coolify resources belong to the Vibn platform itself.
|
||||
// Agents must never be allowed to push code or trigger deployments here.
|
||||
// Read-only operations (list, read file, get status) are still permitted
|
||||
// so agents can observe platform state, but all mutations are blocked.
|
||||
// =============================================================================
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PROTECTED_COOLIFY_APPS = exports.PROTECTED_COOLIFY_PROJECT = exports.PROTECTED_GITEA_REPOS = void 0;
|
||||
exports.assertGiteaWritable = assertGiteaWritable;
|
||||
exports.assertCoolifyDeployable = assertCoolifyDeployable;
|
||||
/** Gitea repos agents can NEVER push to, commit to, or write issues on. */
|
||||
exports.PROTECTED_GITEA_REPOS = new Set([
|
||||
'mark/vibn-frontend',
|
||||
'mark/vibn-agent-runner',
|
||||
'mark/vibn-api',
|
||||
'mark/master-ai',
|
||||
]);
|
||||
/** Coolify project UUID for the VIBN platform — agents cannot deploy here. */
|
||||
exports.PROTECTED_COOLIFY_PROJECT = 'f4owwggokksgw0ogo0844os0';
|
||||
/**
|
||||
* Specific Coolify app UUIDs that must never be deployed by an agent.
|
||||
* Belt-and-suspenders check in case the project UUID filter is bypassed.
|
||||
*/
|
||||
exports.PROTECTED_COOLIFY_APPS = new Set([
|
||||
'y4cscsc8s08c8808go0448s0', // vibn-frontend
|
||||
'kggs4ogckc0w8ggwkkk88kck', // vibn-postgres
|
||||
'o4wwck0g0c04wgoo4g4s0004', // gitea
|
||||
]);
|
||||
function assertGiteaWritable(repo) {
|
||||
if (exports.PROTECTED_GITEA_REPOS.has(repo)) {
|
||||
throw new Error(`SECURITY: Repo "${repo}" is a protected Vibn platform repo. ` +
|
||||
`Agents cannot push code or modify issues in this repository.`);
|
||||
}
|
||||
}
|
||||
function assertCoolifyDeployable(appUuid) {
|
||||
if (exports.PROTECTED_COOLIFY_APPS.has(appUuid)) {
|
||||
throw new Error(`SECURITY: App "${appUuid}" is a protected Vibn platform application. ` +
|
||||
`Agents cannot trigger deployments for this application.`);
|
||||
}
|
||||
}
|
||||
1
vibn-agent-runner/dist/tools/shell-api.d.ts
vendored
Normal file
1
vibn-agent-runner/dist/tools/shell-api.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export declare function executeCommand(workspaceRoot: string, command: string, workingDirectory?: string): Promise<unknown>;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user