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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/api/tsconfig.tsbuildinfo

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,22 @@
},
"scripts": {
"build": "tsc -p tsconfig.json",
"prepublishOnly": "pnpm build"
"prepublishOnly": "pnpm build",
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@devflow-modules/vibe-shared": "workspace:*"
},
"devDependencies": {
"@types/node": "^24.10.1",
"tsx": "^4.20.6",
"vitest": "^1.6.1"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"files": ["dist"]
"files": [
"dist"
]
}
15 changes: 10 additions & 5 deletions packages/core/src/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type { VibeRunInput } from "@devflow-modules/vibe-shared";
import { getSkill } from "../skills/index.js";
import type { SkillMap } from "../skills/index.js";

export async function runAgent(input: VibeRunInput): Promise<unknown> {
const skill = getSkill(input.skill);
/**
* Executa skill com tipagem inferida.
*/
export async function runAgent<K extends keyof SkillMap>(
input: VibeRunInput<K>
): Promise<SkillMap[K]["output"]> {
const skill = getSkill(input.skill as K);

// opcional: evento high-level de agent
input.context.telemetry?.onEvent?.({
type: "start",
skill: input.skill,
skill: String(input.skill),
payload: input.payload,
timestamp: new Date().toISOString(),
});
Expand All @@ -16,7 +21,7 @@ export async function runAgent(input: VibeRunInput): Promise<unknown> {

input.context.telemetry?.onEvent?.({
type: "finish",
skill: input.skill,
skill: String(input.skill),
result,
timestamp: new Date().toISOString(),
});
Expand Down
30 changes: 19 additions & 11 deletions packages/core/src/skills/code_review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@ import { z } from "zod";
import { ai } from "@devflow-modules/vibe-shared";
import type { VibeSkillContext } from "@devflow-modules/vibe-shared";

// Estrutura de cada arquivo avaliado
const FileSchema = z.object({
path: z.string(),
content: z.string(),
});

// Entrada esperada da skill (sem defaults, obrigatória)
export const CodeReviewInputSchema = z.object({
files: z.array(FileSchema).min(1),
language: z.string().default("typescript"),
files: z.array(FileSchema).min(1, "Deve haver pelo menos um arquivo"),
language: z.enum(["typescript", "javascript", "python", "java", "csharp"]),
framework: z.string().optional(),
focus: z
.array(
z.enum(["bugs", "style", "performance", "security", "architecture"]),
)
.default(["bugs", "style", "architecture"]),
});
focus: z.array(
z.enum(["bugs", "style", "performance", "security", "architecture"])
),
})

export type CodeReviewInput = z.infer<typeof CodeReviewInputSchema>;
/**
* ✅ Tipo inferido *após* defaults do Zod.
* Agora `language` e `focus` são opcionais também no TypeScript.
*/
export type CodeReviewInput = z.output<typeof CodeReviewInputSchema>;

export interface CodeReviewFinding {
file: string;
Expand All @@ -37,9 +41,13 @@ export interface CodeReviewResult {
};
}

/**
* 🔍 Skill principal de revisão de código
* Analisa os arquivos recebidos e retorna um relatório técnico
*/
export async function runCodeReview(
payload: unknown,
ctx: VibeSkillContext,
payload: CodeReviewInput,
ctx: VibeSkillContext
): Promise<CodeReviewResult> {
const input = CodeReviewInputSchema.parse(payload);

Expand Down
45 changes: 38 additions & 7 deletions packages/core/src/skills/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
import { runCodeReview } from "./code_review.js";
import type { VibeSkillContext } from "@devflow-modules/vibe-shared";
import type { VibeSkillContext, SkillMapBase } from "@devflow-modules/vibe-shared";
import type { CodeReviewInput, CodeReviewResult } from "./code_review.js";

export type SkillRunner = (payload: unknown, ctx: VibeSkillContext) => Promise<unknown>;
/**
* 🔧 Extende o SkillMapBase do shared com as skills locais.
* Isso torna as skills visíveis em todo o monorepo.
*/
declare module "@devflow-modules/vibe-shared" {
interface SkillMapBase {
code_review: {
input: CodeReviewInput;
output: CodeReviewResult;
};
}
}

// Mapa resultante após o merge
export type SkillMap = SkillMapBase;

/**
* Runner genérico por skill — cada skill tem input/output tipados.
*/
export type SkillRunner<K extends keyof SkillMap> = (
payload: SkillMap[K]["input"],
ctx: VibeSkillContext
) => Promise<SkillMap[K]["output"]>;

const skills: Record<string, SkillRunner> = {
code_review: runCodeReview,
/**
* Registro das skills disponíveis no core.
*/
const skills: { [K in keyof SkillMap]: SkillRunner<K> } = {
code_review: async (payload, ctx) => {
const { runCodeReview } = await import("./code_review.js");
return runCodeReview(payload, ctx);
},
};

export function getSkill(name: string): SkillRunner {
/**
* Retorna o runner tipado de uma skill específica.
*/
export function getSkill<K extends keyof SkillMap>(name: K): SkillRunner<K> {
const skill = skills[name];
if (!skill) throw new Error(`Skill not found: ${name}`);
if (!skill) throw new Error(`Skill not found: ${String(name)}`);
return skill;
}
37 changes: 37 additions & 0 deletions packages/core/tests/agent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, it, expect, vi } from "vitest";
import { runAgent } from "../src/index.js";
import type { CodeReviewResult } from "../src/skills/code_review.js";

vi.mock("@devflow-modules/vibe-shared", () => ({
ai: vi.fn().mockResolvedValue({
content: "✅ Código analisado com sucesso.",
raw: {},
}),
}));

describe("runAgent", () => {
it("deve executar sem erros com input mínimo válido", async () => {
const result: CodeReviewResult = await runAgent({
skill: "code_review",
payload: {
files: [
{
path: "src/example.ts",
content: "console.log('Hello World')",
},
],
// necessários apenas para satisfazer o TS (defaults em runtime)
language: "typescript",
focus: ["bugs", "style", "architecture"],
},
context: {
env: "local",
},
});

expect(result).toBeDefined();
expect(result.summary).toContain("sucesso");
expect(result.findings).toBeInstanceOf(Array);
expect(result.metrics).toHaveProperty("filesCount", 1);
});
});
12 changes: 12 additions & 0 deletions packages/core/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
globals: true,
include: ["tests/**/*.test.ts"],
coverage: {
provider: "v8",
reporter: ["text", "html"],
},
},
});
8 changes: 7 additions & 1 deletion packages/shared/dist/aiClient.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
import type { VibeAIRequest, VibeAIResponse } from "./types.js";
export declare function ai(request: VibeAIRequest): Promise<VibeAIResponse>;
/**
* Executa uma chamada ao modelo OpenAI com tipagem segura.
* Tenta parsear o retorno em JSON para tipo <T>, se possível.
*/
export declare function ai<T = unknown>(request: VibeAIRequest): Promise<VibeAIResponse & {
parsed?: T;
}>;
32 changes: 24 additions & 8 deletions packages/shared/dist/aiClient.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/shared/dist/aiClient.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 9 additions & 7 deletions packages/shared/dist/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ export interface VibeAIResponse {
raw: unknown;
content: string;
}
/**
* Evento de telemetria no estilo MCP:
* registra início/fim/erro de uma execução de ferramenta.
*/
export interface VibeTelemetryEvent {
type: "start" | "finish" | "error";
skill: string;
Expand All @@ -35,8 +31,14 @@ export interface VibeSkillContext {
onEvent?(event: VibeTelemetryEvent): void;
};
}
export interface VibeRunInput<TPayload = unknown> {
skill: string;
payload: TPayload;
export interface SkillMapBase {
}
/**
* Entrada genérica para execução de uma skill.
* O tipo de payload e output é inferido automaticamente do SkillMapBase.
*/
export interface VibeRunInput<K extends keyof SkillMapBase = keyof SkillMapBase> {
skill: K;
payload: SkillMapBase[K]["input"];
context: VibeSkillContext;
}
39 changes: 28 additions & 11 deletions packages/shared/src/aiClient.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,59 @@
import * as path from "path";
import { fileURLToPath } from "url";
import dotenv from "dotenv";
import OpenAI from "openai";
import type { VibeAIRequest, VibeAIResponse } from "./types.js";

// Corrige caminho absoluto da raiz (funciona em Windows)
// =========================
// 🌱 Carrega variáveis de ambiente
// =========================
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const envPath = path.resolve(__dirname, "../../../.env.local");
dotenv.config({ path: envPath });

import OpenAI from "openai";
import type { VibeAIRequest, VibeAIResponse } from "./types.js";

if (!process.env.OPENAI_API_KEY) {
console.error(`❌ OPENAI_API_KEY não encontrada.
Verifique o arquivo .env.local na raiz (${envPath})`);
process.exit(1);
}

// =========================
// 🤖 Cliente OpenAI
// =========================
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});

export async function ai(request: VibeAIRequest): Promise<VibeAIResponse> {
// =========================
// 🧠 Função principal com tipagem genérica
// =========================
/**
* Executa uma chamada ao modelo OpenAI com tipagem segura.
* Tenta parsear o retorno em JSON para tipo <T>, se possível.
*/
export async function ai<T = unknown>(
request: VibeAIRequest
): Promise<VibeAIResponse & { parsed?: T }> {
const completion = await client.chat.completions.create({
model: request.model ?? "gpt-4o-mini",
messages: request.messages,
temperature: request.temperature ?? 0.2,
max_tokens: request.maxTokens ?? 2000,
metadata: {
source: "vibe-intel/aiClient",
skill: request.skill ?? "unknown",
env: request.env ?? "unknown",
},
// ⚠️ Não enviar metadata (gera erro 400 se 'store' não estiver ativo)
});

const content = completion.choices[0]?.message?.content ?? "";
const content = completion.choices?.[0]?.message?.content ?? "";
let parsed: T | undefined;

try {
parsed = JSON.parse(content) as T;
} catch {
// conteúdo não é JSON — ignora
}

return {
raw: completion,
content,
parsed,
};
}
Loading