feat: Master Orchestrator — persistent chat with full project context and awareness tools
Made-with: Cursor
This commit is contained in:
185
dist/tools.js
vendored
185
dist/tools.js
vendored
@@ -227,6 +227,75 @@ exports.ALL_TOOLS = [
|
||||
},
|
||||
required: ['agent', 'task', 'repo']
|
||||
}
|
||||
},
|
||||
// -------------------------------------------------------------------------
|
||||
// Orchestrator-only tools — project-wide awareness
|
||||
// -------------------------------------------------------------------------
|
||||
{
|
||||
name: 'list_repos',
|
||||
description: 'List all Git repositories in the Gitea organization. Returns repo names, descriptions, and last update time.',
|
||||
parameters: { 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.',
|
||||
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"' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'list_all_apps',
|
||||
description: 'List all Coolify applications across all projects with their status (running/stopped/error) and domain.',
|
||||
parameters: { 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.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
app_name: { type: 'string', description: 'Application name (e.g. "vibn-frontend") or UUID' }
|
||||
},
|
||||
required: ['app_name']
|
||||
}
|
||||
},
|
||||
{
|
||||
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']
|
||||
}
|
||||
},
|
||||
{
|
||||
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']
|
||||
}
|
||||
},
|
||||
{
|
||||
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']
|
||||
}
|
||||
}
|
||||
];
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -250,6 +319,14 @@ async function executeTool(name, args, ctx) {
|
||||
case 'gitea_list_issues': return giteaListIssues(String(args.repo), args.state || 'open', ctx);
|
||||
case 'gitea_close_issue': return giteaCloseIssue(String(args.repo), Number(args.issue_number), ctx);
|
||||
case 'spawn_agent': return spawnAgentTool(String(args.agent), String(args.task), String(args.repo), ctx);
|
||||
// Orchestrator tools
|
||||
case 'list_repos': return listRepos(ctx);
|
||||
case 'list_all_issues': return listAllIssues(args.repo, args.state || 'open', ctx);
|
||||
case 'list_all_apps': return listAllApps(ctx);
|
||||
case 'get_app_status': return getAppStatus(String(args.app_name), ctx);
|
||||
case 'read_repo_file': return readRepoFile(String(args.repo), String(args.path), ctx);
|
||||
case 'get_job_status': return getJobStatus(String(args.job_id));
|
||||
case 'deploy_app': return deployApp(String(args.app_name), ctx);
|
||||
default:
|
||||
return { error: `Unknown tool: ${name}` };
|
||||
}
|
||||
@@ -475,3 +552,111 @@ async function spawnAgentTool(agent, task, repo, _ctx) {
|
||||
return { error: `Failed to spawn agent: ${err instanceof Error ? err.message : String(err)}` };
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Orchestrator tools — project-wide awareness
|
||||
// ---------------------------------------------------------------------------
|
||||
async function listRepos(ctx) {
|
||||
const res = await fetch(`${ctx.gitea.apiUrl}/api/v1/repos/search?limit=50&token=${ctx.gitea.apiToken}`, {
|
||||
headers: { 'Authorization': `token ${ctx.gitea.apiToken}` }
|
||||
});
|
||||
const data = await res.json();
|
||||
return (data.data || []).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(repo, state, ctx) {
|
||||
if (repo) {
|
||||
return giteaFetch(`/repos/${repo}/issues?state=${state}&limit=20`, ctx);
|
||||
}
|
||||
// Fetch across all repos
|
||||
const repos = await listRepos(ctx);
|
||||
const allIssues = [];
|
||||
for (const r of repos.slice(0, 10)) {
|
||||
const issues = await giteaFetch(`/repos/${r.name}/issues?state=${state}&limit=10`, ctx);
|
||||
if (Array.isArray(issues)) {
|
||||
allIssues.push(...issues.map((i) => ({
|
||||
repo: r.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 listAllApps(ctx) {
|
||||
const apps = await coolifyFetch('/applications', ctx);
|
||||
if (!Array.isArray(apps))
|
||||
return apps;
|
||||
return apps.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(appName, ctx) {
|
||||
const apps = await coolifyFetch('/applications', ctx);
|
||||
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` };
|
||||
const logs = await coolifyFetch(`/applications/${app.uuid}/logs?limit=20`, ctx);
|
||||
return { name: app.name, uuid: app.uuid, status: app.status, fqdn: app.fqdn, logs };
|
||||
}
|
||||
async function readRepoFile(repo, filePath, ctx) {
|
||||
try {
|
||||
const res = await fetch(`${ctx.gitea.apiUrl}/api/v1/repos/${repo}/contents/${filePath}`, {
|
||||
headers: { 'Authorization': `token ${ctx.gitea.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)}` };
|
||||
}
|
||||
}
|
||||
async function getJobStatus(jobId) {
|
||||
const runnerUrl = process.env.AGENT_RUNNER_URL || 'http://localhost:3333';
|
||||
try {
|
||||
const res = await fetch(`${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)}` };
|
||||
}
|
||||
}
|
||||
async function deployApp(appName, ctx) {
|
||||
const apps = await coolifyFetch('/applications', ctx);
|
||||
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` };
|
||||
const result = await fetch(`${ctx.coolify.apiUrl}/api/v1/deploy?uuid=${app.uuid}&force=false`, {
|
||||
headers: { 'Authorization': `Bearer ${ctx.coolify.apiToken}` }
|
||||
});
|
||||
return result.json();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user