@skroyc/librarian 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -16
- package/dist/agents/context-schema.d.ts +1 -1
- package/dist/agents/context-schema.d.ts.map +1 -1
- package/dist/agents/context-schema.js +5 -2
- package/dist/agents/context-schema.js.map +1 -1
- package/dist/agents/react-agent.d.ts.map +1 -1
- package/dist/agents/react-agent.js +63 -170
- package/dist/agents/react-agent.js.map +1 -1
- package/dist/agents/tool-runtime.d.ts.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +53 -49
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +115 -69
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +246 -150
- package/dist/index.js.map +1 -1
- package/dist/tools/file-finding.tool.d.ts +1 -1
- package/dist/tools/file-finding.tool.d.ts.map +1 -1
- package/dist/tools/file-finding.tool.js +70 -130
- package/dist/tools/file-finding.tool.js.map +1 -1
- package/dist/tools/file-listing.tool.d.ts +7 -1
- package/dist/tools/file-listing.tool.d.ts.map +1 -1
- package/dist/tools/file-listing.tool.js +96 -80
- package/dist/tools/file-listing.tool.js.map +1 -1
- package/dist/tools/file-reading.tool.d.ts +4 -1
- package/dist/tools/file-reading.tool.d.ts.map +1 -1
- package/dist/tools/file-reading.tool.js +107 -45
- package/dist/tools/file-reading.tool.js.map +1 -1
- package/dist/tools/grep-content.tool.d.ts +13 -1
- package/dist/tools/grep-content.tool.d.ts.map +1 -1
- package/dist/tools/grep-content.tool.js +186 -144
- package/dist/tools/grep-content.tool.js.map +1 -1
- package/dist/utils/error-utils.d.ts +9 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/error-utils.js +61 -0
- package/dist/utils/error-utils.js.map +1 -0
- package/dist/utils/file-utils.d.ts +1 -0
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +81 -9
- package/dist/utils/file-utils.js.map +1 -1
- package/dist/utils/format-utils.d.ts +25 -0
- package/dist/utils/format-utils.d.ts.map +1 -0
- package/dist/utils/format-utils.js +111 -0
- package/dist/utils/format-utils.js.map +1 -0
- package/dist/utils/gitignore-service.d.ts +10 -0
- package/dist/utils/gitignore-service.d.ts.map +1 -0
- package/dist/utils/gitignore-service.js +91 -0
- package/dist/utils/gitignore-service.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -2
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +35 -34
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/path-utils.js +3 -3
- package/dist/utils/path-utils.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/context-schema.ts +5 -2
- package/src/agents/react-agent.ts +694 -784
- package/src/agents/tool-runtime.ts +4 -4
- package/src/cli.ts +95 -57
- package/src/config.ts +192 -90
- package/src/index.ts +402 -180
- package/src/tools/file-finding.tool.ts +198 -310
- package/src/tools/file-listing.tool.ts +245 -202
- package/src/tools/file-reading.tool.ts +225 -138
- package/src/tools/grep-content.tool.ts +387 -307
- package/src/utils/error-utils.ts +95 -0
- package/src/utils/file-utils.ts +104 -19
- package/src/utils/format-utils.ts +190 -0
- package/src/utils/gitignore-service.ts +123 -0
- package/src/utils/logger.ts +112 -77
- package/src/utils/path-utils.ts +3 -3
package/src/index.ts
CHANGED
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
* Main entry point for the application
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
import { logger } from
|
|
14
|
-
import os from 'node:os';
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { checkout, clone, fetch } from "isomorphic-git";
|
|
10
|
+
import http from "isomorphic-git/http/node";
|
|
11
|
+
import type { AgentContext } from "./agents/context-schema.js";
|
|
12
|
+
import { ReactAgent } from "./agents/react-agent.js";
|
|
13
|
+
import { logger } from "./utils/logger.js";
|
|
15
14
|
|
|
16
15
|
export interface LibrarianConfig {
|
|
17
16
|
technologies: {
|
|
@@ -24,7 +23,14 @@ export interface LibrarianConfig {
|
|
|
24
23
|
};
|
|
25
24
|
};
|
|
26
25
|
aiProvider: {
|
|
27
|
-
type:
|
|
26
|
+
type:
|
|
27
|
+
| "openai"
|
|
28
|
+
| "anthropic"
|
|
29
|
+
| "google"
|
|
30
|
+
| "openai-compatible"
|
|
31
|
+
| "anthropic-compatible"
|
|
32
|
+
| "claude-code"
|
|
33
|
+
| "gemini-cli";
|
|
28
34
|
apiKey: string;
|
|
29
35
|
model?: string;
|
|
30
36
|
baseURL?: string;
|
|
@@ -39,53 +45,67 @@ export class Librarian {
|
|
|
39
45
|
constructor(config: LibrarianConfig) {
|
|
40
46
|
// Validate AI provider type
|
|
41
47
|
const validProviderTypes = [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
"openai",
|
|
49
|
+
"anthropic",
|
|
50
|
+
"google",
|
|
51
|
+
"openai-compatible",
|
|
52
|
+
"anthropic-compatible",
|
|
53
|
+
"claude-code",
|
|
54
|
+
"gemini-cli",
|
|
49
55
|
] as const;
|
|
50
|
-
type ValidProviderType = typeof validProviderTypes[number];
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
56
|
+
type ValidProviderType = (typeof validProviderTypes)[number];
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
!validProviderTypes.includes(config.aiProvider.type as ValidProviderType)
|
|
60
|
+
) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Unsupported AI provider type: ${config.aiProvider.type}`
|
|
63
|
+
);
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
this.config = config;
|
|
57
67
|
|
|
58
|
-
logger.info(
|
|
68
|
+
logger.info("LIBRARIAN", "Initializing librarian", {
|
|
59
69
|
aiProviderType: config.aiProvider.type,
|
|
60
70
|
model: config.aiProvider.model,
|
|
61
|
-
workingDir: config.workingDir.replace(os.homedir(),
|
|
62
|
-
reposPath: config.repos_path
|
|
71
|
+
workingDir: config.workingDir.replace(os.homedir(), "~"),
|
|
72
|
+
reposPath: config.repos_path
|
|
73
|
+
? config.repos_path.replace(os.homedir(), "~")
|
|
74
|
+
: "workingDir",
|
|
63
75
|
});
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
async initialize(): Promise<void> {
|
|
67
79
|
// Check if Claude CLI is available if using claude-code provider
|
|
68
|
-
if (this.config.aiProvider.type ===
|
|
80
|
+
if (this.config.aiProvider.type === "claude-code") {
|
|
69
81
|
try {
|
|
70
|
-
const { execSync } = await import(
|
|
71
|
-
execSync(
|
|
72
|
-
logger.info(
|
|
82
|
+
const { execSync } = await import("node:child_process");
|
|
83
|
+
execSync("claude --version", { stdio: "ignore" });
|
|
84
|
+
logger.info("LIBRARIAN", "Claude CLI verified");
|
|
73
85
|
} catch {
|
|
74
|
-
logger.error(
|
|
75
|
-
|
|
86
|
+
logger.error("LIBRARIAN", "Claude CLI not found in PATH", undefined, {
|
|
87
|
+
type: "claude-code",
|
|
88
|
+
});
|
|
89
|
+
console.error(
|
|
90
|
+
'Error: "claude" CLI not found. Please install it to use the "claude-code" provider.'
|
|
91
|
+
);
|
|
76
92
|
process.exit(1);
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
// Check if Gemini CLI is available if using gemini-cli provider
|
|
81
|
-
if (this.config.aiProvider.type ===
|
|
97
|
+
if (this.config.aiProvider.type === "gemini-cli") {
|
|
82
98
|
try {
|
|
83
|
-
const { execSync } = await import(
|
|
84
|
-
execSync(
|
|
85
|
-
logger.info(
|
|
99
|
+
const { execSync } = await import("node:child_process");
|
|
100
|
+
execSync("gemini --version", { stdio: "ignore" });
|
|
101
|
+
logger.info("LIBRARIAN", "Gemini CLI verified");
|
|
86
102
|
} catch {
|
|
87
|
-
logger.error(
|
|
88
|
-
|
|
103
|
+
logger.error("LIBRARIAN", "Gemini CLI not found in PATH", undefined, {
|
|
104
|
+
type: "gemini-cli",
|
|
105
|
+
});
|
|
106
|
+
console.error(
|
|
107
|
+
'Error: "gemini" CLI not found. Please install it to use the "gemini-cli" provider.'
|
|
108
|
+
);
|
|
89
109
|
process.exit(1);
|
|
90
110
|
}
|
|
91
111
|
}
|
|
@@ -93,25 +113,31 @@ export class Librarian {
|
|
|
93
113
|
// Create working directory if it doesn't exist
|
|
94
114
|
const workDir = this.config.repos_path || this.config.workingDir;
|
|
95
115
|
if (fs.existsSync(workDir)) {
|
|
96
|
-
logger.debug(
|
|
116
|
+
logger.debug("LIBRARIAN", "Working directory already exists", {
|
|
117
|
+
path: workDir.replace(os.homedir(), "~"),
|
|
118
|
+
});
|
|
97
119
|
} else {
|
|
98
|
-
logger.info(
|
|
120
|
+
logger.info("LIBRARIAN", "Creating working directory", {
|
|
121
|
+
path: workDir.replace(os.homedir(), "~"),
|
|
122
|
+
});
|
|
99
123
|
fs.mkdirSync(workDir, { recursive: true });
|
|
100
124
|
}
|
|
101
125
|
|
|
102
|
-
logger.info(
|
|
126
|
+
logger.info("LIBRARIAN", "Initialization complete");
|
|
103
127
|
}
|
|
104
128
|
|
|
105
|
-
resolveTechnology(
|
|
106
|
-
|
|
129
|
+
resolveTechnology(
|
|
130
|
+
qualifiedName: string
|
|
131
|
+
): { name: string; group: string; repo: string; branch: string } | undefined {
|
|
132
|
+
logger.debug("LIBRARIAN", "Resolving technology", { qualifiedName });
|
|
107
133
|
|
|
108
134
|
let group: string | undefined;
|
|
109
135
|
let name: string;
|
|
110
136
|
|
|
111
|
-
if (qualifiedName.includes(
|
|
112
|
-
const parts = qualifiedName.split(
|
|
137
|
+
if (qualifiedName.includes(":")) {
|
|
138
|
+
const parts = qualifiedName.split(":");
|
|
113
139
|
group = parts[0];
|
|
114
|
-
name = parts[1] ||
|
|
140
|
+
name = parts[1] || "";
|
|
115
141
|
} else {
|
|
116
142
|
name = qualifiedName;
|
|
117
143
|
}
|
|
@@ -120,30 +146,63 @@ export class Librarian {
|
|
|
120
146
|
const groupTechs = this.config.technologies[group];
|
|
121
147
|
const tech = groupTechs ? groupTechs[name] : undefined;
|
|
122
148
|
if (tech) {
|
|
123
|
-
const result = {
|
|
124
|
-
|
|
149
|
+
const result = {
|
|
150
|
+
name,
|
|
151
|
+
group,
|
|
152
|
+
repo: tech.repo,
|
|
153
|
+
branch: tech.branch || "main",
|
|
154
|
+
};
|
|
155
|
+
logger.debug("LIBRARIAN", "Technology resolved with explicit group", {
|
|
156
|
+
name,
|
|
157
|
+
group,
|
|
158
|
+
repoHost: tech.repo.split("/")[2] || "unknown",
|
|
159
|
+
});
|
|
125
160
|
return result;
|
|
126
161
|
}
|
|
127
162
|
} else {
|
|
128
|
-
for (const [groupName, techs] of Object.entries(
|
|
163
|
+
for (const [groupName, techs] of Object.entries(
|
|
164
|
+
this.config.technologies
|
|
165
|
+
)) {
|
|
129
166
|
const tech = techs[name];
|
|
130
167
|
if (tech) {
|
|
131
|
-
const result = {
|
|
132
|
-
|
|
168
|
+
const result = {
|
|
169
|
+
name,
|
|
170
|
+
group: groupName,
|
|
171
|
+
repo: tech.repo,
|
|
172
|
+
branch: tech.branch || "main",
|
|
173
|
+
};
|
|
174
|
+
logger.debug("LIBRARIAN", "Technology resolved by search", {
|
|
175
|
+
name,
|
|
176
|
+
group: groupName,
|
|
177
|
+
repoHost: tech.repo.split("/")[2] || "unknown",
|
|
178
|
+
});
|
|
133
179
|
return result;
|
|
134
180
|
}
|
|
135
181
|
}
|
|
136
182
|
}
|
|
137
|
-
logger.debug(
|
|
183
|
+
logger.debug("LIBRARIAN", "Technology not found in configuration", {
|
|
184
|
+
qualifiedName,
|
|
185
|
+
});
|
|
138
186
|
return undefined;
|
|
139
187
|
}
|
|
140
188
|
|
|
141
189
|
private getSecureGroupPath(groupName: string): string {
|
|
142
|
-
logger.debug(
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
190
|
+
logger.debug("PATH", "Validating group path", { groupName });
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
groupName.includes("../") ||
|
|
194
|
+
groupName.includes("..\\") ||
|
|
195
|
+
groupName.startsWith("..")
|
|
196
|
+
) {
|
|
197
|
+
logger.error(
|
|
198
|
+
"PATH",
|
|
199
|
+
"Group name contains path traversal characters",
|
|
200
|
+
undefined,
|
|
201
|
+
{ groupName }
|
|
202
|
+
);
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Group name "${groupName}" contains invalid path characters`
|
|
205
|
+
);
|
|
147
206
|
}
|
|
148
207
|
|
|
149
208
|
const sanitizedGroupName = path.basename(groupName);
|
|
@@ -154,61 +213,104 @@ export class Librarian {
|
|
|
154
213
|
const resolvedGroupPath = path.resolve(groupPath);
|
|
155
214
|
|
|
156
215
|
if (!resolvedGroupPath.startsWith(resolvedWorkingDir)) {
|
|
157
|
-
logger.error(
|
|
158
|
-
|
|
216
|
+
logger.error(
|
|
217
|
+
"PATH",
|
|
218
|
+
"Group path escapes working directory sandbox",
|
|
219
|
+
undefined,
|
|
220
|
+
{ groupName }
|
|
221
|
+
);
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Group name "${groupName}" attempts to escape the working directory sandbox`
|
|
224
|
+
);
|
|
159
225
|
}
|
|
160
226
|
|
|
161
|
-
logger.debug(
|
|
227
|
+
logger.debug("PATH", "Group path validated", {
|
|
228
|
+
groupName,
|
|
229
|
+
path: groupPath.replace(os.homedir(), "~"),
|
|
230
|
+
});
|
|
162
231
|
return groupPath;
|
|
163
232
|
}
|
|
164
233
|
|
|
165
|
-
private getSecureRepoPath(repoName: string, groupName =
|
|
166
|
-
logger.debug(
|
|
234
|
+
private getSecureRepoPath(repoName: string, groupName = "default"): string {
|
|
235
|
+
logger.debug("PATH", "Validating repo path", { repoName, groupName });
|
|
167
236
|
|
|
168
237
|
// Check for path traversal attempts in the repoName before sanitizing
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
238
|
+
if (
|
|
239
|
+
repoName.includes("../") ||
|
|
240
|
+
repoName.includes("..\\") ||
|
|
241
|
+
repoName.startsWith("..")
|
|
242
|
+
) {
|
|
243
|
+
logger.error(
|
|
244
|
+
"PATH",
|
|
245
|
+
"Repo name contains path traversal characters",
|
|
246
|
+
undefined,
|
|
247
|
+
{ repoName, groupName }
|
|
248
|
+
);
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Repository name "${repoName}" contains invalid path characters`
|
|
251
|
+
);
|
|
172
252
|
}
|
|
173
|
-
|
|
253
|
+
|
|
174
254
|
// Sanitize the names
|
|
175
255
|
const sanitizedRepoName = path.basename(repoName);
|
|
176
256
|
const groupPath = this.getSecureGroupPath(groupName);
|
|
177
257
|
const repoPath = path.join(groupPath, sanitizedRepoName);
|
|
178
|
-
|
|
258
|
+
|
|
179
259
|
// Verify that the resulting path is within the group directory (which is already sandboxed)
|
|
180
260
|
const resolvedGroupDir = path.resolve(groupPath);
|
|
181
261
|
const resolvedRepoPath = path.resolve(repoPath);
|
|
182
|
-
|
|
262
|
+
|
|
183
263
|
if (!resolvedRepoPath.startsWith(resolvedGroupDir)) {
|
|
184
|
-
logger.error(
|
|
185
|
-
|
|
264
|
+
logger.error("PATH", "Repo path escapes group sandbox", undefined, {
|
|
265
|
+
repoName,
|
|
266
|
+
groupName,
|
|
267
|
+
});
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Repository name "${repoName}" attempts to escape the group sandbox`
|
|
270
|
+
);
|
|
186
271
|
}
|
|
187
272
|
|
|
188
|
-
logger.debug(
|
|
273
|
+
logger.debug("PATH", "Repo path validated", {
|
|
274
|
+
repoName,
|
|
275
|
+
groupName,
|
|
276
|
+
path: repoPath.replace(os.homedir(), "~"),
|
|
277
|
+
});
|
|
189
278
|
return repoPath;
|
|
190
279
|
}
|
|
191
280
|
|
|
192
|
-
async updateRepository(
|
|
193
|
-
|
|
281
|
+
async updateRepository(
|
|
282
|
+
repoName: string,
|
|
283
|
+
groupName = "default"
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
logger.info("GIT", "Updating repository", { repoName, groupName });
|
|
194
286
|
|
|
195
|
-
const timingId = logger.timingStart(
|
|
287
|
+
const timingId = logger.timingStart("updateRepository");
|
|
196
288
|
|
|
197
289
|
const repoPath = this.getSecureRepoPath(repoName, groupName);
|
|
198
|
-
const gitPath = path.join(repoPath,
|
|
290
|
+
const gitPath = path.join(repoPath, ".git");
|
|
199
291
|
|
|
200
292
|
if (!fs.existsSync(repoPath)) {
|
|
201
|
-
logger.error(
|
|
202
|
-
|
|
293
|
+
logger.error("GIT", "Repository path does not exist", undefined, {
|
|
294
|
+
repoName,
|
|
295
|
+
repoPath: repoPath.replace(os.homedir(), "~"),
|
|
296
|
+
});
|
|
297
|
+
throw new Error(
|
|
298
|
+
`Repository ${repoName} does not exist at ${repoPath}. Cannot update.`
|
|
299
|
+
);
|
|
203
300
|
}
|
|
204
301
|
|
|
205
302
|
if (!fs.existsSync(gitPath)) {
|
|
206
|
-
logger.error(
|
|
207
|
-
|
|
303
|
+
logger.error("GIT", "Directory is not a git repository", undefined, {
|
|
304
|
+
repoName,
|
|
305
|
+
repoPath: repoPath.replace(os.homedir(), "~"),
|
|
306
|
+
});
|
|
307
|
+
throw new Error(
|
|
308
|
+
`Directory ${repoName} exists at ${repoPath} but is not a git repository. Cannot update.`
|
|
309
|
+
);
|
|
208
310
|
}
|
|
209
311
|
|
|
210
312
|
// Fetch updates from the remote
|
|
211
|
-
logger.debug(
|
|
313
|
+
logger.debug("GIT", "Fetching updates from remote");
|
|
212
314
|
await fetch({
|
|
213
315
|
fs,
|
|
214
316
|
http,
|
|
@@ -216,75 +318,104 @@ export class Librarian {
|
|
|
216
318
|
singleBranch: true,
|
|
217
319
|
});
|
|
218
320
|
|
|
219
|
-
const tech =
|
|
220
|
-
|
|
321
|
+
const tech =
|
|
322
|
+
this.resolveTechnology(`${groupName}:${repoName}`) ||
|
|
323
|
+
this.resolveTechnology(repoName);
|
|
324
|
+
const branch = tech?.branch || "main";
|
|
221
325
|
|
|
222
326
|
// Checkout the latest version
|
|
223
|
-
logger.debug(
|
|
327
|
+
logger.debug("GIT", "Checking out branch", { branch });
|
|
224
328
|
await checkout({
|
|
225
329
|
fs,
|
|
226
330
|
dir: repoPath,
|
|
227
331
|
ref: `origin/${branch}`,
|
|
228
332
|
});
|
|
229
333
|
|
|
230
|
-
logger.timingEnd(timingId,
|
|
334
|
+
logger.timingEnd(timingId, "GIT", `Repository updated: ${repoName}`);
|
|
231
335
|
}
|
|
232
336
|
|
|
233
337
|
async syncRepository(repoName: string): Promise<string> {
|
|
234
|
-
logger.info(
|
|
338
|
+
logger.info("GIT", "Syncing repository", { repoName });
|
|
235
339
|
|
|
236
340
|
const tech = this.resolveTechnology(repoName);
|
|
237
341
|
|
|
238
342
|
if (!tech) {
|
|
239
|
-
logger.error(
|
|
343
|
+
logger.error("GIT", "Technology not found in configuration", undefined, {
|
|
344
|
+
repoName,
|
|
345
|
+
});
|
|
240
346
|
throw new Error(`Repository ${repoName} not found in configuration`);
|
|
241
347
|
}
|
|
242
348
|
|
|
243
349
|
const repoPath = this.getSecureRepoPath(tech.name, tech.group);
|
|
244
350
|
|
|
245
351
|
// Check if this is a local path (not a remote URL)
|
|
246
|
-
const isLocalRepo = !(
|
|
352
|
+
const isLocalRepo = !(
|
|
353
|
+
tech.repo.startsWith("http://") || tech.repo.startsWith("https://")
|
|
354
|
+
);
|
|
247
355
|
|
|
248
356
|
if (fs.existsSync(repoPath)) {
|
|
249
357
|
// Repository exists
|
|
250
358
|
if (isLocalRepo) {
|
|
251
359
|
// For local repos, skip git operations - just use existing files
|
|
252
|
-
logger.debug(
|
|
360
|
+
logger.debug("GIT", "Local repository exists, skipping git operations");
|
|
253
361
|
return repoPath;
|
|
254
362
|
}
|
|
255
363
|
// Remote repository, update it (no-op for local repos to avoid isomorphic-git issues with local paths)
|
|
256
364
|
if (!isLocalRepo) {
|
|
257
|
-
logger.debug(
|
|
365
|
+
logger.debug("GIT", "Repository exists, performing update");
|
|
258
366
|
await this.updateRepository(tech.name, tech.group);
|
|
259
367
|
}
|
|
260
368
|
} else {
|
|
261
369
|
// Repository doesn't exist
|
|
262
370
|
if (isLocalRepo) {
|
|
263
371
|
// Local repo doesn't exist - this is an error in test setup
|
|
264
|
-
logger.error(
|
|
265
|
-
|
|
372
|
+
logger.error("GIT", "Local repository path does not exist", undefined, {
|
|
373
|
+
repoName,
|
|
374
|
+
repoPath,
|
|
375
|
+
});
|
|
376
|
+
throw new Error(
|
|
377
|
+
`Local repository ${repoName} does not exist at ${repoPath}`
|
|
378
|
+
);
|
|
266
379
|
}
|
|
267
380
|
// Remote repository, clone it
|
|
268
|
-
logger.debug(
|
|
269
|
-
return await this.cloneRepository(
|
|
381
|
+
logger.debug("GIT", "Repository does not exist, performing clone");
|
|
382
|
+
return await this.cloneRepository(
|
|
383
|
+
tech.name,
|
|
384
|
+
tech.repo,
|
|
385
|
+
tech.group,
|
|
386
|
+
tech.branch
|
|
387
|
+
);
|
|
270
388
|
}
|
|
271
389
|
|
|
272
390
|
return repoPath;
|
|
273
391
|
}
|
|
274
392
|
|
|
275
|
-
async cloneRepository(
|
|
276
|
-
|
|
393
|
+
async cloneRepository(
|
|
394
|
+
repoName: string,
|
|
395
|
+
repoUrl: string,
|
|
396
|
+
groupName = "default",
|
|
397
|
+
branch = "main"
|
|
398
|
+
): Promise<string> {
|
|
399
|
+
logger.info("GIT", "Cloning repository", {
|
|
400
|
+
repoName,
|
|
401
|
+
repoHost: repoUrl.split("/")[2] || "unknown",
|
|
402
|
+
branch,
|
|
403
|
+
groupName,
|
|
404
|
+
});
|
|
277
405
|
|
|
278
|
-
const timingId = logger.timingStart(
|
|
406
|
+
const timingId = logger.timingStart("cloneRepository");
|
|
279
407
|
|
|
280
408
|
const repoPath = this.getSecureRepoPath(repoName, groupName);
|
|
281
409
|
|
|
282
410
|
// Check if repository already exists
|
|
283
411
|
if (fs.existsSync(repoPath)) {
|
|
284
412
|
// Check if it's a git repository by checking for .git folder
|
|
285
|
-
const gitPath = path.join(repoPath,
|
|
413
|
+
const gitPath = path.join(repoPath, ".git");
|
|
286
414
|
if (fs.existsSync(gitPath)) {
|
|
287
|
-
logger.debug(
|
|
415
|
+
logger.debug(
|
|
416
|
+
"GIT",
|
|
417
|
+
"Repository already exists as git repo, updating instead"
|
|
418
|
+
);
|
|
288
419
|
await this.updateRepository(repoName, groupName);
|
|
289
420
|
return repoPath;
|
|
290
421
|
}
|
|
@@ -293,11 +424,14 @@ export class Librarian {
|
|
|
293
424
|
// Ensure group directory exists
|
|
294
425
|
const groupPath = this.getSecureGroupPath(groupName);
|
|
295
426
|
if (!fs.existsSync(groupPath)) {
|
|
296
|
-
logger.debug(
|
|
427
|
+
logger.debug("GIT", "Creating group directory", {
|
|
428
|
+
groupName,
|
|
429
|
+
path: groupPath.replace(os.homedir(), "~"),
|
|
430
|
+
});
|
|
297
431
|
fs.mkdirSync(groupPath, { recursive: true });
|
|
298
432
|
}
|
|
299
433
|
|
|
300
|
-
logger.debug(
|
|
434
|
+
logger.debug("GIT", "Starting shallow clone", { depth: 1 });
|
|
301
435
|
await clone({
|
|
302
436
|
fs,
|
|
303
437
|
http,
|
|
@@ -314,7 +448,7 @@ export class Librarian {
|
|
|
314
448
|
try {
|
|
315
449
|
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
316
450
|
for (const file of files) {
|
|
317
|
-
if (file.name ===
|
|
451
|
+
if (file.name === ".git") {
|
|
318
452
|
continue;
|
|
319
453
|
}
|
|
320
454
|
if (file.isDirectory()) {
|
|
@@ -329,21 +463,29 @@ export class Librarian {
|
|
|
329
463
|
};
|
|
330
464
|
countFiles(repoPath);
|
|
331
465
|
|
|
332
|
-
logger.debug(
|
|
466
|
+
logger.debug("GIT", "Clone completed", { fileCount });
|
|
333
467
|
|
|
334
|
-
logger.timingEnd(timingId,
|
|
468
|
+
logger.timingEnd(timingId, "GIT", `Repository cloned: ${repoName}`);
|
|
335
469
|
return repoPath;
|
|
336
470
|
}
|
|
337
471
|
|
|
338
472
|
async queryRepository(repoName: string, query: string): Promise<string> {
|
|
339
|
-
logger.info(
|
|
473
|
+
logger.info("LIBRARIAN", "Querying repository", {
|
|
474
|
+
repoName,
|
|
475
|
+
queryLength: query.length,
|
|
476
|
+
});
|
|
340
477
|
|
|
341
|
-
const timingId = logger.timingStart(
|
|
478
|
+
const timingId = logger.timingStart("queryRepository");
|
|
342
479
|
|
|
343
480
|
const tech = this.resolveTechnology(repoName);
|
|
344
481
|
|
|
345
482
|
if (!tech) {
|
|
346
|
-
logger.error(
|
|
483
|
+
logger.error(
|
|
484
|
+
"LIBRARIAN",
|
|
485
|
+
"Technology not found in configuration",
|
|
486
|
+
undefined,
|
|
487
|
+
{ repoName }
|
|
488
|
+
);
|
|
347
489
|
throw new Error(`Repository ${repoName} not found in configuration`);
|
|
348
490
|
}
|
|
349
491
|
|
|
@@ -357,10 +499,10 @@ export class Librarian {
|
|
|
357
499
|
technology: tech.name,
|
|
358
500
|
};
|
|
359
501
|
|
|
360
|
-
logger.debug(
|
|
361
|
-
workingDir: context.workingDir.replace(os.homedir(),
|
|
502
|
+
logger.debug("LIBRARIAN", "Initializing agent for query with context", {
|
|
503
|
+
workingDir: context.workingDir.replace(os.homedir(), "~"),
|
|
362
504
|
group: context.group,
|
|
363
|
-
technology: context.technology
|
|
505
|
+
technology: context.technology,
|
|
364
506
|
});
|
|
365
507
|
|
|
366
508
|
// Initialize the agent
|
|
@@ -370,29 +512,43 @@ export class Librarian {
|
|
|
370
512
|
technology: {
|
|
371
513
|
name: tech.name,
|
|
372
514
|
repository: tech.repo,
|
|
373
|
-
branch: tech.branch
|
|
374
|
-
}
|
|
515
|
+
branch: tech.branch,
|
|
516
|
+
},
|
|
375
517
|
});
|
|
376
518
|
await agent.initialize();
|
|
377
519
|
|
|
378
520
|
// Execute the query using the agent with context
|
|
379
521
|
const result = await agent.queryRepository(repoPath, query, context);
|
|
380
522
|
|
|
381
|
-
logger.timingEnd(timingId,
|
|
382
|
-
logger.info(
|
|
523
|
+
logger.timingEnd(timingId, "LIBRARIAN", `Query completed: ${repoName}`);
|
|
524
|
+
logger.info("LIBRARIAN", "Query result received", {
|
|
525
|
+
repoName,
|
|
526
|
+
responseLength: result.length,
|
|
527
|
+
});
|
|
383
528
|
|
|
384
529
|
return result;
|
|
385
530
|
}
|
|
386
531
|
|
|
387
|
-
async *streamRepository(
|
|
388
|
-
|
|
532
|
+
async *streamRepository(
|
|
533
|
+
repoName: string,
|
|
534
|
+
query: string
|
|
535
|
+
): AsyncGenerator<string, void, unknown> {
|
|
536
|
+
logger.info("LIBRARIAN", "Streaming repository query", {
|
|
537
|
+
repoName,
|
|
538
|
+
queryLength: query.length,
|
|
539
|
+
});
|
|
389
540
|
|
|
390
|
-
const timingId = logger.timingStart(
|
|
541
|
+
const timingId = logger.timingStart("streamRepository");
|
|
391
542
|
|
|
392
543
|
const tech = this.resolveTechnology(repoName);
|
|
393
544
|
|
|
394
545
|
if (!tech) {
|
|
395
|
-
logger.error(
|
|
546
|
+
logger.error(
|
|
547
|
+
"LIBRARIAN",
|
|
548
|
+
"Technology not found in configuration",
|
|
549
|
+
undefined,
|
|
550
|
+
{ repoName }
|
|
551
|
+
);
|
|
396
552
|
throw new Error(`Repository ${repoName} not found in configuration`);
|
|
397
553
|
}
|
|
398
554
|
|
|
@@ -403,9 +559,9 @@ export class Librarian {
|
|
|
403
559
|
};
|
|
404
560
|
|
|
405
561
|
// Listen for interruption signals (Ctrl+C)
|
|
406
|
-
logger.debug(
|
|
407
|
-
process.on(
|
|
408
|
-
process.on(
|
|
562
|
+
logger.debug("LIBRARIAN", "Setting up interruption handlers");
|
|
563
|
+
process.on("SIGINT", cleanup);
|
|
564
|
+
process.on("SIGTERM", cleanup);
|
|
409
565
|
|
|
410
566
|
try {
|
|
411
567
|
// Clone or sync repository first
|
|
@@ -413,8 +569,10 @@ export class Librarian {
|
|
|
413
569
|
|
|
414
570
|
// Check for interruption after sync
|
|
415
571
|
if (isInterrupted) {
|
|
416
|
-
logger.warn(
|
|
417
|
-
|
|
572
|
+
logger.warn("LIBRARIAN", "Repository sync interrupted by user", {
|
|
573
|
+
repoName,
|
|
574
|
+
});
|
|
575
|
+
yield "[Repository sync interrupted by user]";
|
|
418
576
|
return;
|
|
419
577
|
}
|
|
420
578
|
|
|
@@ -425,11 +583,15 @@ export class Librarian {
|
|
|
425
583
|
technology: tech.name,
|
|
426
584
|
};
|
|
427
585
|
|
|
428
|
-
logger.debug(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
586
|
+
logger.debug(
|
|
587
|
+
"LIBRARIAN",
|
|
588
|
+
"Initializing agent for streaming query with context",
|
|
589
|
+
{
|
|
590
|
+
workingDir: context.workingDir.replace(os.homedir(), "~"),
|
|
591
|
+
group: context.group,
|
|
592
|
+
technology: context.technology,
|
|
593
|
+
}
|
|
594
|
+
);
|
|
433
595
|
|
|
434
596
|
// Initialize agent
|
|
435
597
|
const agent = new ReactAgent({
|
|
@@ -438,56 +600,71 @@ export class Librarian {
|
|
|
438
600
|
technology: {
|
|
439
601
|
name: tech.name,
|
|
440
602
|
repository: tech.repo,
|
|
441
|
-
branch: tech.branch
|
|
442
|
-
}
|
|
603
|
+
branch: tech.branch,
|
|
604
|
+
},
|
|
443
605
|
});
|
|
444
606
|
await agent.initialize();
|
|
445
607
|
|
|
446
608
|
// Check for interruption after initialization
|
|
447
609
|
if (isInterrupted) {
|
|
448
|
-
logger.warn(
|
|
449
|
-
|
|
610
|
+
logger.warn("LIBRARIAN", "Agent initialization interrupted by user", {
|
|
611
|
+
repoName,
|
|
612
|
+
});
|
|
613
|
+
yield "[Agent initialization interrupted by user]";
|
|
450
614
|
return;
|
|
451
615
|
}
|
|
452
616
|
|
|
453
617
|
// Execute streaming query using agent with context
|
|
454
|
-
logger.debug(
|
|
618
|
+
logger.debug("LIBRARIAN", "Starting stream from agent");
|
|
455
619
|
yield* agent.streamRepository(repoPath, query, context);
|
|
456
620
|
} catch (error) {
|
|
457
621
|
// Handle repository-level errors
|
|
458
|
-
let errorMessage =
|
|
622
|
+
let errorMessage = "Unknown error";
|
|
459
623
|
|
|
460
624
|
if (error instanceof Error) {
|
|
461
|
-
if (error.message.includes(
|
|
625
|
+
if (error.message.includes("not found in configuration")) {
|
|
462
626
|
errorMessage = error.message;
|
|
463
|
-
} else if (
|
|
627
|
+
} else if (
|
|
628
|
+
error.message.includes("git") ||
|
|
629
|
+
error.message.includes("clone")
|
|
630
|
+
) {
|
|
464
631
|
errorMessage = `Repository operation failed: ${error.message}`;
|
|
465
|
-
} else if (error.message.includes(
|
|
466
|
-
errorMessage =
|
|
632
|
+
} else if (error.message.includes("timeout")) {
|
|
633
|
+
errorMessage = "Repository operation timed out";
|
|
467
634
|
} else {
|
|
468
635
|
errorMessage = `Repository error: ${error.message}`;
|
|
469
636
|
}
|
|
470
637
|
}
|
|
471
638
|
|
|
472
|
-
logger.error(
|
|
639
|
+
logger.error(
|
|
640
|
+
"LIBRARIAN",
|
|
641
|
+
"Stream error",
|
|
642
|
+
error instanceof Error ? error : new Error(errorMessage),
|
|
643
|
+
{ repoName }
|
|
644
|
+
);
|
|
473
645
|
yield `\n[Error: ${errorMessage}]`;
|
|
474
646
|
throw error;
|
|
475
647
|
} finally {
|
|
476
648
|
// Clean up event listeners
|
|
477
|
-
process.removeListener(
|
|
478
|
-
process.removeListener(
|
|
479
|
-
logger.timingEnd(timingId,
|
|
649
|
+
process.removeListener("SIGINT", cleanup);
|
|
650
|
+
process.removeListener("SIGTERM", cleanup);
|
|
651
|
+
logger.timingEnd(timingId, "LIBRARIAN", `Stream completed: ${repoName}`);
|
|
480
652
|
}
|
|
481
653
|
}
|
|
482
654
|
|
|
483
655
|
async queryGroup(groupName: string, query: string): Promise<string> {
|
|
484
|
-
logger.info(
|
|
656
|
+
logger.info("LIBRARIAN", "Querying group", {
|
|
657
|
+
groupName,
|
|
658
|
+
queryLength: query.length,
|
|
659
|
+
});
|
|
485
660
|
|
|
486
|
-
const timingId = logger.timingStart(
|
|
661
|
+
const timingId = logger.timingStart("queryGroup");
|
|
487
662
|
|
|
488
663
|
// Validate group exists
|
|
489
664
|
if (!this.config.technologies[groupName]) {
|
|
490
|
-
logger.error(
|
|
665
|
+
logger.error("LIBRARIAN", "Group not found in configuration", undefined, {
|
|
666
|
+
groupName,
|
|
667
|
+
});
|
|
491
668
|
throw new Error(`Group ${groupName} not found in configuration`);
|
|
492
669
|
}
|
|
493
670
|
|
|
@@ -498,7 +675,10 @@ export class Librarian {
|
|
|
498
675
|
const technologies = this.config.technologies[groupName];
|
|
499
676
|
if (technologies) {
|
|
500
677
|
const techNames = Object.keys(technologies);
|
|
501
|
-
logger.info(
|
|
678
|
+
logger.info("LIBRARIAN", "Syncing all technologies in group", {
|
|
679
|
+
groupName,
|
|
680
|
+
techCount: techNames.length,
|
|
681
|
+
});
|
|
502
682
|
for (const techName of techNames) {
|
|
503
683
|
await this.syncRepository(techName);
|
|
504
684
|
}
|
|
@@ -508,37 +688,56 @@ export class Librarian {
|
|
|
508
688
|
const context: AgentContext = {
|
|
509
689
|
workingDir: groupPath,
|
|
510
690
|
group: groupName,
|
|
511
|
-
technology:
|
|
691
|
+
technology: "", // No specific technology for group-level queries
|
|
512
692
|
};
|
|
513
693
|
|
|
514
|
-
logger.debug(
|
|
515
|
-
|
|
516
|
-
group
|
|
517
|
-
|
|
694
|
+
logger.debug(
|
|
695
|
+
"LIBRARIAN",
|
|
696
|
+
"Initializing agent for group query with context",
|
|
697
|
+
{
|
|
698
|
+
workingDir: context.workingDir.replace(os.homedir(), "~"),
|
|
699
|
+
group: context.group,
|
|
700
|
+
}
|
|
701
|
+
);
|
|
518
702
|
|
|
519
703
|
// Initialize the agent with the group directory as working directory
|
|
520
704
|
const agent = new ReactAgent({
|
|
521
705
|
aiProvider: this.config.aiProvider,
|
|
522
|
-
workingDir: groupPath
|
|
706
|
+
workingDir: groupPath,
|
|
523
707
|
});
|
|
524
708
|
await agent.initialize();
|
|
525
709
|
|
|
526
710
|
// Execute the query using the agent with context
|
|
527
711
|
const result = await agent.queryRepository(groupPath, query, context);
|
|
528
712
|
|
|
529
|
-
logger.timingEnd(
|
|
530
|
-
|
|
713
|
+
logger.timingEnd(
|
|
714
|
+
timingId,
|
|
715
|
+
"LIBRARIAN",
|
|
716
|
+
`Group query completed: ${groupName}`
|
|
717
|
+
);
|
|
718
|
+
logger.info("LIBRARIAN", "Group query result received", {
|
|
719
|
+
groupName,
|
|
720
|
+
responseLength: result.length,
|
|
721
|
+
});
|
|
531
722
|
|
|
532
723
|
return result;
|
|
533
724
|
}
|
|
534
725
|
|
|
535
|
-
async *streamGroup(
|
|
536
|
-
|
|
726
|
+
async *streamGroup(
|
|
727
|
+
groupName: string,
|
|
728
|
+
query: string
|
|
729
|
+
): AsyncGenerator<string, void, unknown> {
|
|
730
|
+
logger.info("LIBRARIAN", "Streaming group query", {
|
|
731
|
+
groupName,
|
|
732
|
+
queryLength: query.length,
|
|
733
|
+
});
|
|
537
734
|
|
|
538
|
-
const timingId = logger.timingStart(
|
|
735
|
+
const timingId = logger.timingStart("streamGroup");
|
|
539
736
|
|
|
540
737
|
if (!this.config.technologies[groupName]) {
|
|
541
|
-
logger.error(
|
|
738
|
+
logger.error("LIBRARIAN", "Group not found in configuration", undefined, {
|
|
739
|
+
groupName,
|
|
740
|
+
});
|
|
542
741
|
throw new Error(`Group ${groupName} not found in configuration`);
|
|
543
742
|
}
|
|
544
743
|
|
|
@@ -549,80 +748,103 @@ export class Librarian {
|
|
|
549
748
|
isInterrupted = true;
|
|
550
749
|
};
|
|
551
750
|
|
|
552
|
-
logger.debug(
|
|
553
|
-
process.on(
|
|
554
|
-
process.on(
|
|
751
|
+
logger.debug("LIBRARIAN", "Setting up interruption handlers for group");
|
|
752
|
+
process.on("SIGINT", cleanup);
|
|
753
|
+
process.on("SIGTERM", cleanup);
|
|
555
754
|
|
|
556
755
|
try {
|
|
557
756
|
const technologies = this.config.technologies[groupName];
|
|
558
757
|
if (technologies) {
|
|
559
758
|
const techNames = Object.keys(technologies);
|
|
560
|
-
logger.info(
|
|
759
|
+
logger.info(
|
|
760
|
+
"LIBRARIAN",
|
|
761
|
+
"Syncing all technologies in group for streaming",
|
|
762
|
+
{ groupName, techCount: techNames.length }
|
|
763
|
+
);
|
|
561
764
|
for (const techName of techNames) {
|
|
562
765
|
await this.syncRepository(techName);
|
|
563
766
|
}
|
|
564
767
|
}
|
|
565
768
|
|
|
566
769
|
if (isInterrupted) {
|
|
567
|
-
logger.warn(
|
|
568
|
-
|
|
770
|
+
logger.warn("LIBRARIAN", "Group sync interrupted by user", {
|
|
771
|
+
groupName,
|
|
772
|
+
});
|
|
773
|
+
yield "[Repository sync interrupted by user]";
|
|
569
774
|
return;
|
|
570
775
|
}
|
|
571
776
|
|
|
572
777
|
const context: AgentContext = {
|
|
573
778
|
workingDir: groupPath,
|
|
574
779
|
group: groupName,
|
|
575
|
-
technology:
|
|
780
|
+
technology: "",
|
|
576
781
|
};
|
|
577
782
|
|
|
578
|
-
logger.debug(
|
|
579
|
-
|
|
580
|
-
group
|
|
581
|
-
|
|
783
|
+
logger.debug(
|
|
784
|
+
"LIBRARIAN",
|
|
785
|
+
"Initializing agent for group streaming with context",
|
|
786
|
+
{
|
|
787
|
+
workingDir: context.workingDir.replace(os.homedir(), "~"),
|
|
788
|
+
group: context.group,
|
|
789
|
+
}
|
|
790
|
+
);
|
|
582
791
|
|
|
583
792
|
const agent = new ReactAgent({
|
|
584
793
|
aiProvider: this.config.aiProvider,
|
|
585
|
-
workingDir: groupPath
|
|
794
|
+
workingDir: groupPath,
|
|
586
795
|
});
|
|
587
796
|
await agent.initialize();
|
|
588
797
|
|
|
589
798
|
if (isInterrupted) {
|
|
590
|
-
logger.warn(
|
|
591
|
-
|
|
799
|
+
logger.warn(
|
|
800
|
+
"LIBRARIAN",
|
|
801
|
+
"Agent initialization interrupted by user for group",
|
|
802
|
+
{ groupName }
|
|
803
|
+
);
|
|
804
|
+
yield "[Agent initialization interrupted by user]";
|
|
592
805
|
return;
|
|
593
806
|
}
|
|
594
807
|
|
|
595
|
-
logger.debug(
|
|
808
|
+
logger.debug("LIBRARIAN", "Starting stream from agent for group");
|
|
596
809
|
yield* agent.streamRepository(groupPath, query, context);
|
|
597
810
|
} catch (error) {
|
|
598
811
|
const errorMessage = this.getGroupStreamErrorMessage(error);
|
|
599
|
-
logger.error(
|
|
812
|
+
logger.error(
|
|
813
|
+
"LIBRARIAN",
|
|
814
|
+
"Group stream error",
|
|
815
|
+
error instanceof Error ? error : new Error(errorMessage),
|
|
816
|
+
{ groupName }
|
|
817
|
+
);
|
|
600
818
|
yield `\n[Error: ${errorMessage}]`;
|
|
601
819
|
throw error;
|
|
602
820
|
} finally {
|
|
603
|
-
process.removeListener(
|
|
604
|
-
process.removeListener(
|
|
605
|
-
logger.timingEnd(
|
|
821
|
+
process.removeListener("SIGINT", cleanup);
|
|
822
|
+
process.removeListener("SIGTERM", cleanup);
|
|
823
|
+
logger.timingEnd(
|
|
824
|
+
timingId,
|
|
825
|
+
"LIBRARIAN",
|
|
826
|
+
`Group stream completed: ${groupName}`
|
|
827
|
+
);
|
|
606
828
|
}
|
|
607
829
|
}
|
|
608
830
|
|
|
609
831
|
private getGroupStreamErrorMessage(error: unknown): string {
|
|
610
832
|
if (!(error instanceof Error)) {
|
|
611
|
-
return
|
|
833
|
+
return "Unknown error";
|
|
612
834
|
}
|
|
613
835
|
|
|
614
|
-
if (error.message.includes(
|
|
836
|
+
if (error.message.includes("not found in configuration")) {
|
|
615
837
|
return error.message;
|
|
616
838
|
}
|
|
617
839
|
|
|
618
|
-
if (error.message.includes(
|
|
840
|
+
if (error.message.includes("git") || error.message.includes("clone")) {
|
|
619
841
|
return `Repository operation failed: ${error.message}`;
|
|
620
842
|
}
|
|
621
843
|
|
|
622
|
-
if (error.message.includes(
|
|
623
|
-
return
|
|
844
|
+
if (error.message.includes("timeout")) {
|
|
845
|
+
return "Repository operation timed out";
|
|
624
846
|
}
|
|
625
847
|
|
|
626
848
|
return `Group error: ${error.message}`;
|
|
627
849
|
}
|
|
628
|
-
}
|
|
850
|
+
}
|