File size: 4,532 Bytes
61b1da1
a5d1c1b
 
 
 
 
 
 
 
 
 
 
 
071f828
 
 
 
 
 
61b1da1
 
071f828
 
 
 
a5d1c1b
 
071f828
61b1da1
a5d1c1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
071f828
61b1da1
a5d1c1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
071f828
61b1da1
a5d1c1b
 
 
 
 
 
 
 
 
 
 
 
61b1da1
 
a5d1c1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
071f828
61b1da1
a5d1c1b
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import { RepoMetadata, FilesResponse, SearchResponse } from '@ever-brain/api/contract';

export class CliApiClient {
  constructor(private baseUrl: string) {}

  setBaseUrl(url: string): void {
    this.baseUrl = url;
  }

  private cleanUrl(endpoint: string): string {
    return `${this.baseUrl.replace(/\/+$/, '')}${endpoint}`;
  }

  private async safeFetch(url: string, init?: RequestInit): Promise<Response> {
    try {
      return await fetch(url, init);
    } catch (err: unknown) {
      const errorMsg = err instanceof Error ? err.message : String(err);
      throw new Error(
        `Cannot connect to Ever-Brain API at ${this.baseUrl} (${errorMsg}).\n` +
        `Is the Ever-Brain server running? Start it with 'pnpm dev' (or configure --api-url / EVB_API_URL).`
      );
    }
  }

  async getRepoMetadata(owner: string, repo: string): Promise<RepoMetadata> {
    const url = this.cleanUrl(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`);
    const res = await this.safeFetch(url, {
      headers: { 'User-Agent': 'Ever-Brain-CLI/1.0', Accept: 'application/json' },
    });

    if (!res.ok) {
      let message = `Failed to resolve repository ${owner}/${repo} (${res.status})`;
      try {
        const body = (await res.json()) as { error?: string };
        if (body.error) message = body.error;
      } catch {}
      throw new Error(message);
    }

    return (await res.json()) as RepoMetadata;
  }

  async getFiles(
    owner: string,
    repo: string,
    prefix?: string,
    fresh?: boolean,
    branch?: string
  ): Promise<FilesResponse> {
    const params = new URLSearchParams();
    if (prefix) params.set('prefix', prefix);
    if (fresh) params.set('fresh', 'true');
    if (branch) params.set('branch', branch);

    const queryString = params.toString() ? `?${params.toString()}` : '';
    const url = this.cleanUrl(
      `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/files${queryString}`
    );

    const res = await this.safeFetch(url, {
      headers: { 'User-Agent': 'Ever-Brain-CLI/1.0', Accept: 'application/json' },
    });

    if (!res.ok) {
      let message = `Failed to fetch files for ${owner}/${repo} (${res.status})`;
      try {
        const body = (await res.json()) as { error?: string };
        if (body.error) message = body.error;
      } catch {}
      throw new Error(message);
    }

    return (await res.json()) as FilesResponse;
  }

  async getFileContent(
    owner: string,
    repo: string,
    filePath: string,
    branch?: string
  ): Promise<{ content: string; exactPath: string; branch: string }> {
    const cleanPath = filePath.replace(/^\/+/, '');
    const params = new URLSearchParams();
    if (branch) params.set('branch', branch);

    const queryString = params.toString() ? `?${params.toString()}` : '';
    const url = this.cleanUrl(
      `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/file/${cleanPath}${queryString}`
    );

    const res = await this.safeFetch(url, {
      headers: { 'User-Agent': 'Ever-Brain-CLI/1.0' },
    });

    if (!res.ok) {
      let message = `File '${cleanPath}' not found in ${owner}/${repo}`;
      try {
        const body = (await res.json()) as { error?: string };
        if (body.error) message = body.error;
      } catch {}
      throw new Error(message);
    }

    const content = await res.text();
    const exactPath = res.headers.get('x-ever-brain-path') || cleanPath;
    const resolvedBranch = res.headers.get('x-ever-brain-branch') || branch || 'main';

    return {
      content,
      exactPath,
      branch: resolvedBranch,
    };
  }

  async searchFiles(
    owner: string,
    repo: string,
    query: string,
    prefix?: string,
    branch?: string
  ): Promise<SearchResponse> {
    const params = new URLSearchParams({ q: query });
    if (prefix) params.set('prefix', prefix);
    if (branch) params.set('branch', branch);

    const url = this.cleanUrl(
      `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/search?${params.toString()}`
    );

    const res = await this.safeFetch(url, {
      headers: { 'User-Agent': 'Ever-Brain-CLI/1.0', Accept: 'application/json' },
    });

    if (!res.ok) {
      let message = `Search failed for ${owner}/${repo} (${res.status})`;
      try {
        const body = (await res.json()) as { error?: string };
        if (body.error) message = body.error;
      } catch {}
      throw new Error(message);
    }

    return (await res.json()) as SearchResponse;
  }
}