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
13 changes: 13 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "agentvision/agentvision" }
],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch"
}
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
ci:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]

steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm --filter @agentvision/core build

- run: pnpm typecheck

- run: pnpm build

- run: pnpm test
49 changes: 49 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Publish

on:
push:
branches: [main]

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm build
- run: pnpm test

publish:
needs: ci
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
registry-url: https://registry.npmjs.org
- run: pnpm install --frozen-lockfile
- run: pnpm build
- uses: changesets/action@v1
with:
version: pnpm changeset version
publish: pnpm changeset publish
title: "chore: version packages"
commit: "chore: version packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
"build": "pnpm -r build",
"dev": "pnpm --filter agentvision dev",
"lint": "pnpm -r lint",
"typecheck": "pnpm -r typecheck"
"typecheck": "pnpm -r typecheck",
"test": "vitest run",
"test:watch": "vitest",
"changeset": "changeset"
},
"devDependencies": {
"typescript": "^5.7.0"
"@changesets/changelog-github": "^0.6.0",
"@changesets/cli": "^2.30.0",
"typescript": "^5.7.0",
"vitest": "^4.1.0"
},
"packageManager": "pnpm@10.29.3",
"engines": {
"node": ">=20"
}
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
"dist/",
"README.md"
],
"repository": {
"type": "git",
"url": "https://github.com/agentvision/agentvision.git",
"directory": "packages/cli"
},
"keywords": [
"ai",
"agent",
Expand All @@ -33,5 +38,9 @@
"@types/node": "^25.5.0",
"tsup": "^8.4.0",
"typescript": "^5.7.0"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
11 changes: 11 additions & 0 deletions packages/core/src/__fixtures__/test-project-1/.mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
},
"remote-server": {
"url": "https://mcp.example.com/api"
}
}
}
7 changes: 7 additions & 0 deletions packages/core/src/__fixtures__/test-project-1/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Project Instructions

This is a test project for Claude Code.

## Rules
- Use TypeScript
- Write tests
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
}
}
2 changes: 2 additions & 0 deletions packages/core/src/__fixtures__/test-project-2/.cursorrules
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Use TypeScript with strict mode enabled.
Prefer functional patterns over classes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use TypeScript.
3 changes: 3 additions & 0 deletions packages/core/src/__fixtures__/test-project-3-multi/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Instructions

Multi-agent project.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ invalid json here
Empty file.
68 changes: 68 additions & 0 deletions packages/core/src/agents/agents.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, it, expect } from "vitest";
import { agents, getAgent, getAgentIds } from "./index.js";

describe("agent registry", () => {
it("exports all 5 agents", () => {
expect(agents).toHaveLength(5);
});

it("each agent has required fields", () => {
for (const agent of agents) {
expect(agent.id).toBeTruthy();
expect(agent.name).toBeTruthy();
expect(agent.description).toBeTruthy();
expect(agent.docsUrl).toMatch(/^https?:\/\//);
expect(agent.configs.length).toBeGreaterThan(0);
}
});

it("agent IDs are unique", () => {
const ids = agents.map((a) => a.id);
expect(new Set(ids).size).toBe(ids.length);
});

it("getAgent returns correct agent by ID", () => {
const claude = getAgent("claude-code");
expect(claude).toBeDefined();
expect(claude!.name).toBe("Claude Code");
});

it("getAgent returns undefined for unknown ID", () => {
expect(getAgent("nonexistent")).toBeUndefined();
});

it("getAgentIds returns all IDs", () => {
const ids = getAgentIds();
expect(ids).toContain("claude-code");
expect(ids).toContain("cursor");
expect(ids).toContain("codex");
expect(ids).toContain("gemini-cli");
expect(ids).toContain("github-copilot");
});

it("each config location has valid category", () => {
const validCategories = ["instructions", "mcp-servers", "settings", "skills", "rules", "context"];
for (const agent of agents) {
for (const config of agent.configs) {
expect(validCategories).toContain(config.category);
}
}
});

it("each config location has valid format", () => {
const validFormats = ["json", "jsonc", "yaml", "markdown", "toml", "text", "directory"];
for (const agent of agents) {
for (const config of agent.configs) {
expect(validFormats).toContain(config.format);
}
}
});

it("agents with MCP extraction have extractMcpServers function", () => {
const withMcp = ["claude-code", "cursor", "gemini-cli"];
for (const id of withMcp) {
const agent = getAgent(id);
expect(agent?.extractMcpServers).toBeTypeOf("function");
}
});
});
79 changes: 79 additions & 0 deletions packages/core/src/comparator/comparator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, it, expect } from "vitest";
import { compare } from "./comparator.js";
import type { AgentScanResult } from "../types.js";
import type { AgentDefinition } from "../agents/types.js";

function makeAgent(id: string, name: string): AgentDefinition {
return { id, name, description: "", docsUrl: "https://example.com", configs: [] };
}

function makeResult(
agent: AgentDefinition,
opts: { detected?: boolean; hasMcp?: boolean; hasInstructions?: boolean; emptyInstructions?: boolean } = {},
): AgentScanResult {
const { detected = true, hasMcp = false, hasInstructions = false, emptyInstructions = false } = opts;
return {
agent,
detected,
configs: hasInstructions || emptyInstructions
? [{
location: { label: "instructions", category: "instructions", paths: [], multi: false, format: "markdown", description: "" },
files: [{
absolutePath: "/test/CLAUDE.md",
relativePath: "./CLAUDE.md",
scope: "project",
size: emptyInstructions ? 0 : 100,
gitignored: false,
status: emptyInstructions ? "empty" : "valid",
}],
}]
: [],
mcpServers: hasMcp
? [{ name: "test", transport: "stdio", command: "node", source: "/test/.mcp.json" }]
: [],
warnings: [],
};
}

describe("compare", () => {
it("returns empty result with fewer than 2 detected agents", () => {
const result = compare([makeResult(makeAgent("a", "A"), { detected: false })]);
expect(result.gaps).toEqual([]);
expect(result.conflicts).toEqual([]);
});

it("detects MCP gap when one agent has MCP and another does not", () => {
const a = makeResult(makeAgent("claude-code", "Claude Code"), { hasMcp: true });
const b = makeResult(makeAgent("cursor", "Cursor"), { hasMcp: false });
const result = compare([a, b]);
const mcpGap = result.gaps.find((g) => g.category === "mcp-servers");
expect(mcpGap).toBeDefined();
expect(mcpGap!.presentIn).toContain("claude-code");
expect(mcpGap!.missingFrom).toContain("cursor");
});

it("detects instructions gap", () => {
const a = makeResult(makeAgent("claude-code", "Claude Code"), { hasInstructions: true });
const b = makeResult(makeAgent("cursor", "Cursor"), { hasInstructions: false });
const result = compare([a, b]);
const instrGap = result.gaps.find((g) => g.category === "instructions" && g.missingFrom.length > 0);
expect(instrGap).toBeDefined();
expect(instrGap!.presentIn).toContain("claude-code");
});

it("detects empty instruction files", () => {
const a = makeResult(makeAgent("claude-code", "Claude Code"), { emptyInstructions: true });
const b = makeResult(makeAgent("cursor", "Cursor"), { hasInstructions: true });
const result = compare([a, b]);
const emptyGap = result.gaps.find((g) => g.description.includes("Empty"));
expect(emptyGap).toBeDefined();
expect(emptyGap!.presentIn).toContain("claude-code");
});

it("reports no gaps when agents are symmetric", () => {
const a = makeResult(makeAgent("a", "A"), { hasMcp: true, hasInstructions: true });
const b = makeResult(makeAgent("b", "B"), { hasMcp: true, hasInstructions: true });
const result = compare([a, b]);
expect(result.gaps).toHaveLength(0);
});
});
Loading
Loading