Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b0a5676
feat: add code graphs
m43i Jun 15, 2026
93a907e
fix: ignore plan files
m43i Jun 15, 2026
10f4484
fix: repair code graph ci
m43i Jun 15, 2026
9eb0817
fix: prepare tree-sitter native bindings in ci
m43i Jun 15, 2026
9584594
fix: address greptile feedback
m43i Jun 15, 2026
a281c4f
fix: test
m43i Jun 15, 2026
38e91c0
Mark graph binding failed on terminal workflow errors
m43i Jun 15, 2026
4c8cbfb
Fallback to full snapshot on GitLab compare timeout
m43i Jun 15, 2026
9de8328
Remove connector UI from backend PR
m43i Jun 15, 2026
a62a6d8
Harden repository loading against symlink escapes
m43i Jun 15, 2026
6b047a3
Store GitHub installation account metadata
m43i Jun 15, 2026
abee5b8
Exclude completed process runs from file conflict lookup
m43i Jun 15, 2026
2e84a4d
Handle missing symlink targets and truncated GitHub trees
m43i Jun 15, 2026
f491466
Handle repository graph sync failures and GitLab errors
m43i Jun 15, 2026
ff5b9d4
Enforce connector ownership for installation actions
m43i Jun 15, 2026
7fac1c0
Add direct repository lookup to connector clients
m43i Jun 15, 2026
9a9da32
Fix worker connector credential import
m43i Jun 15, 2026
7620aa3
Fix external file redirects and provider read concurrency
m43i Jun 15, 2026
05f56c8
Fix repository URL refresh supersession
m43i Jun 15, 2026
0622941
Enforce owner checks for connector installs and graph sync
m43i Jun 15, 2026
da8ad1c
Merge remote-tracking branch 'origin/main' into feature/code-graphs
m43i Jun 19, 2026
86fb0ca
fix(frontend): add code file type metadata
m43i Jun 19, 2026
e2e1fdf
fix(api): avoid repository invalidation on code retry
m43i Jun 19, 2026
15d2f48
fix(worker): scope duplicate webhook events
m43i Jun 19, 2026
3daae8a
Merge branch 'main' into feature/code-graphs
m43i Jun 23, 2026
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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ jobs:

- name: Install dependencies
run: bun install --frozen-lockfile
env:
CXXFLAGS: -std=c++20

- name: Patch tree-sitter Bun native binding
run: |
mkdir -p node_modules/.bun/tree-sitter@0.25.0/node_modules/tree-sitter/prebuilds/linux-x64
cp node_modules/.bun/tree-sitter@0.25.0/node_modules/tree-sitter/build/Release/tree_sitter_runtime_binding.node node_modules/.bun/tree-sitter@0.25.0/node_modules/tree-sitter/prebuilds/linux-x64/tree-sitter.node
cp node_modules/.bun/@tree-sitter-grammars+tree-sitter-zig@1.1.2+7aa2583e580bc282/node_modules/@tree-sitter-grammars/tree-sitter-zig/prebuilds/linux-x64/@tree-sitter-grammars+tree-sitter-zig.node node_modules/.bun/@tree-sitter-grammars+tree-sitter-zig@1.1.2+7aa2583e580bc282/node_modules/@tree-sitter-grammars/tree-sitter-zig/prebuilds/linux-x64/tree-sitter-zig.node

- name: Prepare environment
run: cp .env.sample .env
Expand Down Expand Up @@ -103,6 +111,14 @@ jobs:

- name: Install dependencies
run: bun install --frozen-lockfile
env:
CXXFLAGS: -std=c++20

- name: Patch tree-sitter Bun native binding
run: |
mkdir -p node_modules/.bun/tree-sitter@0.25.0/node_modules/tree-sitter/prebuilds/linux-x64
cp node_modules/.bun/tree-sitter@0.25.0/node_modules/tree-sitter/build/Release/tree_sitter_runtime_binding.node node_modules/.bun/tree-sitter@0.25.0/node_modules/tree-sitter/prebuilds/linux-x64/tree-sitter.node
cp node_modules/.bun/@tree-sitter-grammars+tree-sitter-zig@1.1.2+7aa2583e580bc282/node_modules/@tree-sitter-grammars/tree-sitter-zig/prebuilds/linux-x64/@tree-sitter-grammars+tree-sitter-zig.node node_modules/.bun/@tree-sitter-grammars+tree-sitter-zig@1.1.2+7aa2583e580bc282/node_modules/@tree-sitter-grammars/tree-sitter-zig/prebuilds/linux-x64/tree-sitter-zig.node

- name: Prepare environment
run: cp .env.sample .env
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,6 @@ backend/server.exe

# turbo
.turbo

# AI
./plans
8 changes: 8 additions & 0 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ FROM oven/bun:1 AS build

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
g++ \
make \
python3 \
&& rm -rf /var/lib/apt/lists/*

# Cache workspace dependency installation
COPY package.json bun.lock ./
COPY patches patches
Expand Down Expand Up @@ -59,6 +66,7 @@ RUN apt-get update \
brotli \
bzip2 \
ca-certificates \
git \
ghostscript \
gzip \
libarchive-tools \
Expand Down
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@kiwi/ai": "workspace:*",
"@kiwi/auth": "workspace:*",
"@kiwi/contracts": "workspace:*",
"@kiwi/connectors": "workspace:*",
"@kiwi/db": "workspace:*",
"@kiwi/files": "workspace:*",
"@kiwi/graph": "workspace:*",
Expand Down
90 changes: 90 additions & 0 deletions apps/api/src/lib/__tests__/connectors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { beforeEach, describe, expect, mock, test } from "bun:test";

type TestRepository = {
provider: "github";
id: string;
fullName: string;
name: string;
htmlUrl: string;
defaultBranch: string;
private: boolean;
};

const branchRepositoryCalls: TestRepository[] = [];
let listRepositoriesCalls = 0;

mock.module("@kiwi/connectors", () => ({
createGitHubClient: () => ({
provider: "github",
getRepository: async (repositoryId: string) => ({
provider: "github",
id: "1",
fullName: repositoryId,
name: "app",
htmlUrl: `https://github.com/${repositoryId}`,
defaultBranch: "main",
private: true,
}),
listRepositories: async () => {
listRepositoriesCalls += 1;
return [];
},
listBranches: async (repository: TestRepository) => {
branchRepositoryCalls.push(repository);
return [{ name: "main", commitSha: "commit-sha" }];
},
}),
createGitHubInstallationToken: async () => ({ token: "installation-token", expiresAt: "2026-01-01T01:00:00Z" }),
createGitLabClient: () => {
throw new Error("GitLab client was not expected");
},
getGitHubInstallationAccount: async () => ({
login: "acme",
type: "organization",
repositorySelection: "selected",
}),
}));

mock.module("@kiwi/connectors/credentials", () => ({
decryptConnectorCredentials: () => ({ provider: "github", appId: "app-1", privateKeyPem: "pem" }),
decryptConnectorSecret: () => "secret",
encryptConnectorCredentials: () => "encrypted",
encryptConnectorSecret: () => "encrypted-secret",
}));

mock.module("../env", () => ({
env: {
AUTH_SECRET: "test-secret",
API_URL: "http://localhost:4321",
TRUSTED_ORIGINS: "http://localhost:3000",
},
}));

const { listProviderBranches } = await import("../connectors");

describe("connector library helpers", () => {
beforeEach(() => {
branchRepositoryCalls.length = 0;
listRepositoriesCalls = 0;
});

test("loads branches through direct repository lookup", async () => {
await expect(
listProviderBranches(
{
id: "connector-1",
provider: "github",
encryptedCredentials: "encrypted",
} as never,
{
id: "installation-1",
providerInstallationId: "99",
} as never,
"acme/app"
)
).resolves.toEqual([{ name: "main", commitSha: "commit-sha" }]);

expect(listRepositoriesCalls).toBe(0);
expect(branchRepositoryCalls[0]).toMatchObject({ fullName: "acme/app" });
});
});
177 changes: 177 additions & 0 deletions apps/api/src/lib/__tests__/graph-file-proxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { beforeEach, describe, expect, mock, test } from "bun:test";

let selectedFile: unknown;
let bindingRow: unknown = null;
let metadataCalls = 0;
let streamCalls = 0;
let selectCallCount = 0;
const providerReadCalls: Array<{ path: string; commitSha: string }> = [];

function createSelectQuery() {
const chain = {
from: () => chain,
innerJoin: () => chain,
where: () => chain,
limit: () => {
selectCallCount += 1;
if (selectCallCount === 1) {
return selectedFile ? [selectedFile] : [];
}
return bindingRow ? [bindingRow] : [];
},
};
return chain;
}

mock.module("@kiwi/db", () => ({
db: {
select: () => createSelectQuery(),
},
}));

mock.module("@kiwi/files", () => ({
deleteFile: async () => undefined,
getFileMetadata: async () => {
metadataCalls += 1;
return { size: 12, type: "text/plain", lastModified: null };
},
getFileStream: async () => {
streamCalls += 1;
return { content: new Blob(["hello world\n"]).stream(), size: 12, type: "text/plain" };
},
listFiles: async () => [],
putGraphFile: async () => ({ key: "key", type: "text/plain" }),
}));

mock.module("../connectors", () => ({
createProviderClient: async () => ({
readFile: async (_repository: unknown, path: string, commitSha: string) => {
providerReadCalls.push({ path, commitSha });
return "export const older = true;\n";
},
}),
}));

// Dynamic import is required so module mocks are installed before graph-file-proxy is evaluated.
const { getGraphFileProxyResponse } = await import("../graph-file-proxy");

describe("graph file proxy", () => {
beforeEach(() => {
metadataCalls = 0;
streamCalls = 0;
selectCallCount = 0;
providerReadCalls.length = 0;
bindingRow = null;
selectedFile = {
key: "graphs/graph-1/file-1.txt",
name: "file.txt",
mimeType: "text/plain",
storageKind: "internal",
externalProvider: null,
externalUrl: null,
repositoryBindingId: null,
metadata: null,
};
});

test("streams internal files from S3", async () => {
const result = await getGraphFileProxyResponse({
graphId: "graph-1",
fileId: "file-1",
request: new Request("http://localhost/file"),
bucket: "bucket",
});

expect(result.status).toBe("ok");
expect(metadataCalls).toBe(1);
expect(streamCalls).toBe(1);
if (result.status === "ok") {
expect(result.response.status).toBe(200);
}
});

test("redirects external GitHub files to immutable raw URLs without S3 calls", async () => {
selectedFile = {
key: "external:github:acme/widgets@commit-1:src/index.ts",
name: "widgets/src/index.ts",
mimeType: "text/plain",
storageKind: "external",
externalProvider: "github",
externalUrl: "https://raw.githubusercontent.com/acme/widgets/commit-1/src/index.ts",
repositoryBindingId: null,
metadata: JSON.stringify({
repositoryUrl: "https://github.com/acme/widgets.git",
repositoryName: "widgets",
commitSha: "commit-1",
path: "src/index.ts",
external: {
provider: "github",
rawUrl: "https://raw.githubusercontent.com/acme/widgets/commit-1/src/index.ts",
htmlUrl: "https://github.com/acme/widgets/blob/commit-1/src/index.ts",
},
}),
};

const result = await getGraphFileProxyResponse({
graphId: "graph-1",
fileId: "file-1",
request: new Request("http://localhost/file"),
bucket: "bucket",
});

expect(result.status).toBe("ok");
expect(metadataCalls).toBe(0);
expect(streamCalls).toBe(0);
expect(providerReadCalls).toEqual([]);
if (result.status === "ok") {
expect(result.response.status).toBe(307);
expect(result.response.headers.get("location")).toBe(
"https://raw.githubusercontent.com/acme/widgets/commit-1/src/index.ts"
);
}
});

test("reads connector-backed files at the row commit without S3 access", async () => {
selectedFile = {
key: "connector:binding-1:commit-2:src/index.ts",
name: "widgets/src/index.ts",
mimeType: "text/plain",
storageKind: "external",
externalProvider: "github",
externalUrl: "https://raw.githubusercontent.com/acme/widgets/commit-2/src/index.ts",
repositoryBindingId: "binding-1",
metadata: JSON.stringify({
repositoryUrl: "https://github.com/acme/widgets.git",
repositoryName: "widgets",
commitSha: "commit-1",
path: "src/index.ts",
}),
};
bindingRow = {
binding: {
providerRepositoryId: "1",
repositoryFullName: "acme/widgets",
repositoryHtmlUrl: "https://github.com/acme/widgets",
branch: "main",
},
installation: { status: "active" },
connector: { provider: "github", status: "active" },
};

const result = await getGraphFileProxyResponse({
graphId: "graph-1",
fileId: "file-1",
request: new Request("http://localhost/file"),
bucket: "bucket",
});

expect(result.status).toBe("ok");
expect(metadataCalls).toBe(0);
expect(streamCalls).toBe(0);
expect(providerReadCalls).toEqual([{ path: "src/index.ts", commitSha: "commit-1" }]);
if (result.status === "ok") {
expect(result.response.status).toBe(200);
await expect(result.response.text()).resolves.toBe("export const older = true;\n");
}
});
});
Loading