@skroyc/librarian 0.1.0 → 0.2.0
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 +36 -27
- 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 +667 -641
- 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/dist/index.js
CHANGED
|
@@ -1,76 +1,86 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { checkout, clone, fetch } from "isomorphic-git";
|
|
5
|
+
import http from "isomorphic-git/http/node";
|
|
6
|
+
import { ReactAgent } from "./agents/react-agent.js";
|
|
7
|
+
import { logger } from "./utils/logger.js";
|
|
8
8
|
export class Librarian {
|
|
9
9
|
config;
|
|
10
10
|
constructor(config) {
|
|
11
11
|
const validProviderTypes = [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
"openai",
|
|
13
|
+
"anthropic",
|
|
14
|
+
"google",
|
|
15
|
+
"openai-compatible",
|
|
16
|
+
"anthropic-compatible",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"gemini-cli",
|
|
19
19
|
];
|
|
20
20
|
if (!validProviderTypes.includes(config.aiProvider.type)) {
|
|
21
21
|
throw new Error(`Unsupported AI provider type: ${config.aiProvider.type}`);
|
|
22
22
|
}
|
|
23
23
|
this.config = config;
|
|
24
|
-
logger.info(
|
|
24
|
+
logger.info("LIBRARIAN", "Initializing librarian", {
|
|
25
25
|
aiProviderType: config.aiProvider.type,
|
|
26
26
|
model: config.aiProvider.model,
|
|
27
|
-
workingDir: config.workingDir.replace(os.homedir(),
|
|
28
|
-
reposPath: config.repos_path
|
|
27
|
+
workingDir: config.workingDir.replace(os.homedir(), "~"),
|
|
28
|
+
reposPath: config.repos_path
|
|
29
|
+
? config.repos_path.replace(os.homedir(), "~")
|
|
30
|
+
: "workingDir",
|
|
29
31
|
});
|
|
30
32
|
}
|
|
31
33
|
async initialize() {
|
|
32
|
-
if (this.config.aiProvider.type ===
|
|
34
|
+
if (this.config.aiProvider.type === "claude-code") {
|
|
33
35
|
try {
|
|
34
|
-
const { execSync } = await import(
|
|
35
|
-
execSync(
|
|
36
|
-
logger.info(
|
|
36
|
+
const { execSync } = await import("node:child_process");
|
|
37
|
+
execSync("claude --version", { stdio: "ignore" });
|
|
38
|
+
logger.info("LIBRARIAN", "Claude CLI verified");
|
|
37
39
|
}
|
|
38
40
|
catch {
|
|
39
|
-
logger.error(
|
|
41
|
+
logger.error("LIBRARIAN", "Claude CLI not found in PATH", undefined, {
|
|
42
|
+
type: "claude-code",
|
|
43
|
+
});
|
|
40
44
|
console.error('Error: "claude" CLI not found. Please install it to use the "claude-code" provider.');
|
|
41
45
|
process.exit(1);
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
|
-
if (this.config.aiProvider.type ===
|
|
48
|
+
if (this.config.aiProvider.type === "gemini-cli") {
|
|
45
49
|
try {
|
|
46
|
-
const { execSync } = await import(
|
|
47
|
-
execSync(
|
|
48
|
-
logger.info(
|
|
50
|
+
const { execSync } = await import("node:child_process");
|
|
51
|
+
execSync("gemini --version", { stdio: "ignore" });
|
|
52
|
+
logger.info("LIBRARIAN", "Gemini CLI verified");
|
|
49
53
|
}
|
|
50
54
|
catch {
|
|
51
|
-
logger.error(
|
|
55
|
+
logger.error("LIBRARIAN", "Gemini CLI not found in PATH", undefined, {
|
|
56
|
+
type: "gemini-cli",
|
|
57
|
+
});
|
|
52
58
|
console.error('Error: "gemini" CLI not found. Please install it to use the "gemini-cli" provider.');
|
|
53
59
|
process.exit(1);
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
const workDir = this.config.repos_path || this.config.workingDir;
|
|
57
63
|
if (fs.existsSync(workDir)) {
|
|
58
|
-
logger.debug(
|
|
64
|
+
logger.debug("LIBRARIAN", "Working directory already exists", {
|
|
65
|
+
path: workDir.replace(os.homedir(), "~"),
|
|
66
|
+
});
|
|
59
67
|
}
|
|
60
68
|
else {
|
|
61
|
-
logger.info(
|
|
69
|
+
logger.info("LIBRARIAN", "Creating working directory", {
|
|
70
|
+
path: workDir.replace(os.homedir(), "~"),
|
|
71
|
+
});
|
|
62
72
|
fs.mkdirSync(workDir, { recursive: true });
|
|
63
73
|
}
|
|
64
|
-
logger.info(
|
|
74
|
+
logger.info("LIBRARIAN", "Initialization complete");
|
|
65
75
|
}
|
|
66
76
|
resolveTechnology(qualifiedName) {
|
|
67
|
-
logger.debug(
|
|
77
|
+
logger.debug("LIBRARIAN", "Resolving technology", { qualifiedName });
|
|
68
78
|
let group;
|
|
69
79
|
let name;
|
|
70
|
-
if (qualifiedName.includes(
|
|
71
|
-
const parts = qualifiedName.split(
|
|
80
|
+
if (qualifiedName.includes(":")) {
|
|
81
|
+
const parts = qualifiedName.split(":");
|
|
72
82
|
group = parts[0];
|
|
73
|
-
name = parts[1] ||
|
|
83
|
+
name = parts[1] || "";
|
|
74
84
|
}
|
|
75
85
|
else {
|
|
76
86
|
name = qualifiedName;
|
|
@@ -79,8 +89,17 @@ export class Librarian {
|
|
|
79
89
|
const groupTechs = this.config.technologies[group];
|
|
80
90
|
const tech = groupTechs ? groupTechs[name] : undefined;
|
|
81
91
|
if (tech) {
|
|
82
|
-
const result = {
|
|
83
|
-
|
|
92
|
+
const result = {
|
|
93
|
+
name,
|
|
94
|
+
group,
|
|
95
|
+
repo: tech.repo,
|
|
96
|
+
branch: tech.branch || "main",
|
|
97
|
+
};
|
|
98
|
+
logger.debug("LIBRARIAN", "Technology resolved with explicit group", {
|
|
99
|
+
name,
|
|
100
|
+
group,
|
|
101
|
+
repoHost: tech.repo.split("/")[2] || "unknown",
|
|
102
|
+
});
|
|
84
103
|
return result;
|
|
85
104
|
}
|
|
86
105
|
}
|
|
@@ -88,19 +107,32 @@ export class Librarian {
|
|
|
88
107
|
for (const [groupName, techs] of Object.entries(this.config.technologies)) {
|
|
89
108
|
const tech = techs[name];
|
|
90
109
|
if (tech) {
|
|
91
|
-
const result = {
|
|
92
|
-
|
|
110
|
+
const result = {
|
|
111
|
+
name,
|
|
112
|
+
group: groupName,
|
|
113
|
+
repo: tech.repo,
|
|
114
|
+
branch: tech.branch || "main",
|
|
115
|
+
};
|
|
116
|
+
logger.debug("LIBRARIAN", "Technology resolved by search", {
|
|
117
|
+
name,
|
|
118
|
+
group: groupName,
|
|
119
|
+
repoHost: tech.repo.split("/")[2] || "unknown",
|
|
120
|
+
});
|
|
93
121
|
return result;
|
|
94
122
|
}
|
|
95
123
|
}
|
|
96
124
|
}
|
|
97
|
-
logger.debug(
|
|
125
|
+
logger.debug("LIBRARIAN", "Technology not found in configuration", {
|
|
126
|
+
qualifiedName,
|
|
127
|
+
});
|
|
98
128
|
return undefined;
|
|
99
129
|
}
|
|
100
130
|
getSecureGroupPath(groupName) {
|
|
101
|
-
logger.debug(
|
|
102
|
-
if (groupName.includes(
|
|
103
|
-
|
|
131
|
+
logger.debug("PATH", "Validating group path", { groupName });
|
|
132
|
+
if (groupName.includes("../") ||
|
|
133
|
+
groupName.includes("..\\") ||
|
|
134
|
+
groupName.startsWith("..")) {
|
|
135
|
+
logger.error("PATH", "Group name contains path traversal characters", undefined, { groupName });
|
|
104
136
|
throw new Error(`Group name "${groupName}" contains invalid path characters`);
|
|
105
137
|
}
|
|
106
138
|
const sanitizedGroupName = path.basename(groupName);
|
|
@@ -109,16 +141,21 @@ export class Librarian {
|
|
|
109
141
|
const resolvedWorkingDir = path.resolve(workDir);
|
|
110
142
|
const resolvedGroupPath = path.resolve(groupPath);
|
|
111
143
|
if (!resolvedGroupPath.startsWith(resolvedWorkingDir)) {
|
|
112
|
-
logger.error(
|
|
144
|
+
logger.error("PATH", "Group path escapes working directory sandbox", undefined, { groupName });
|
|
113
145
|
throw new Error(`Group name "${groupName}" attempts to escape the working directory sandbox`);
|
|
114
146
|
}
|
|
115
|
-
logger.debug(
|
|
147
|
+
logger.debug("PATH", "Group path validated", {
|
|
148
|
+
groupName,
|
|
149
|
+
path: groupPath.replace(os.homedir(), "~"),
|
|
150
|
+
});
|
|
116
151
|
return groupPath;
|
|
117
152
|
}
|
|
118
|
-
getSecureRepoPath(repoName, groupName =
|
|
119
|
-
logger.debug(
|
|
120
|
-
if (repoName.includes(
|
|
121
|
-
|
|
153
|
+
getSecureRepoPath(repoName, groupName = "default") {
|
|
154
|
+
logger.debug("PATH", "Validating repo path", { repoName, groupName });
|
|
155
|
+
if (repoName.includes("../") ||
|
|
156
|
+
repoName.includes("..\\") ||
|
|
157
|
+
repoName.startsWith("..")) {
|
|
158
|
+
logger.error("PATH", "Repo name contains path traversal characters", undefined, { repoName, groupName });
|
|
122
159
|
throw new Error(`Repository name "${repoName}" contains invalid path characters`);
|
|
123
160
|
}
|
|
124
161
|
const sanitizedRepoName = path.basename(repoName);
|
|
@@ -127,89 +164,116 @@ export class Librarian {
|
|
|
127
164
|
const resolvedGroupDir = path.resolve(groupPath);
|
|
128
165
|
const resolvedRepoPath = path.resolve(repoPath);
|
|
129
166
|
if (!resolvedRepoPath.startsWith(resolvedGroupDir)) {
|
|
130
|
-
logger.error(
|
|
167
|
+
logger.error("PATH", "Repo path escapes group sandbox", undefined, {
|
|
168
|
+
repoName,
|
|
169
|
+
groupName,
|
|
170
|
+
});
|
|
131
171
|
throw new Error(`Repository name "${repoName}" attempts to escape the group sandbox`);
|
|
132
172
|
}
|
|
133
|
-
logger.debug(
|
|
173
|
+
logger.debug("PATH", "Repo path validated", {
|
|
174
|
+
repoName,
|
|
175
|
+
groupName,
|
|
176
|
+
path: repoPath.replace(os.homedir(), "~"),
|
|
177
|
+
});
|
|
134
178
|
return repoPath;
|
|
135
179
|
}
|
|
136
|
-
async updateRepository(repoName, groupName =
|
|
137
|
-
logger.info(
|
|
138
|
-
const timingId = logger.timingStart(
|
|
180
|
+
async updateRepository(repoName, groupName = "default") {
|
|
181
|
+
logger.info("GIT", "Updating repository", { repoName, groupName });
|
|
182
|
+
const timingId = logger.timingStart("updateRepository");
|
|
139
183
|
const repoPath = this.getSecureRepoPath(repoName, groupName);
|
|
140
|
-
const gitPath = path.join(repoPath,
|
|
184
|
+
const gitPath = path.join(repoPath, ".git");
|
|
141
185
|
if (!fs.existsSync(repoPath)) {
|
|
142
|
-
logger.error(
|
|
186
|
+
logger.error("GIT", "Repository path does not exist", undefined, {
|
|
187
|
+
repoName,
|
|
188
|
+
repoPath: repoPath.replace(os.homedir(), "~"),
|
|
189
|
+
});
|
|
143
190
|
throw new Error(`Repository ${repoName} does not exist at ${repoPath}. Cannot update.`);
|
|
144
191
|
}
|
|
145
192
|
if (!fs.existsSync(gitPath)) {
|
|
146
|
-
logger.error(
|
|
193
|
+
logger.error("GIT", "Directory is not a git repository", undefined, {
|
|
194
|
+
repoName,
|
|
195
|
+
repoPath: repoPath.replace(os.homedir(), "~"),
|
|
196
|
+
});
|
|
147
197
|
throw new Error(`Directory ${repoName} exists at ${repoPath} but is not a git repository. Cannot update.`);
|
|
148
198
|
}
|
|
149
|
-
logger.debug(
|
|
199
|
+
logger.debug("GIT", "Fetching updates from remote");
|
|
150
200
|
await fetch({
|
|
151
201
|
fs,
|
|
152
202
|
http,
|
|
153
203
|
dir: repoPath,
|
|
154
204
|
singleBranch: true,
|
|
155
205
|
});
|
|
156
|
-
const tech = this.resolveTechnology(`${groupName}:${repoName}`) ||
|
|
157
|
-
|
|
158
|
-
|
|
206
|
+
const tech = this.resolveTechnology(`${groupName}:${repoName}`) ||
|
|
207
|
+
this.resolveTechnology(repoName);
|
|
208
|
+
const branch = tech?.branch || "main";
|
|
209
|
+
logger.debug("GIT", "Checking out branch", { branch });
|
|
159
210
|
await checkout({
|
|
160
211
|
fs,
|
|
161
212
|
dir: repoPath,
|
|
162
213
|
ref: `origin/${branch}`,
|
|
163
214
|
});
|
|
164
|
-
logger.timingEnd(timingId,
|
|
215
|
+
logger.timingEnd(timingId, "GIT", `Repository updated: ${repoName}`);
|
|
165
216
|
}
|
|
166
217
|
async syncRepository(repoName) {
|
|
167
|
-
logger.info(
|
|
218
|
+
logger.info("GIT", "Syncing repository", { repoName });
|
|
168
219
|
const tech = this.resolveTechnology(repoName);
|
|
169
220
|
if (!tech) {
|
|
170
|
-
logger.error(
|
|
221
|
+
logger.error("GIT", "Technology not found in configuration", undefined, {
|
|
222
|
+
repoName,
|
|
223
|
+
});
|
|
171
224
|
throw new Error(`Repository ${repoName} not found in configuration`);
|
|
172
225
|
}
|
|
173
226
|
const repoPath = this.getSecureRepoPath(tech.name, tech.group);
|
|
174
|
-
const isLocalRepo = !(tech.repo.startsWith(
|
|
227
|
+
const isLocalRepo = !(tech.repo.startsWith("http://") || tech.repo.startsWith("https://"));
|
|
175
228
|
if (fs.existsSync(repoPath)) {
|
|
176
229
|
if (isLocalRepo) {
|
|
177
|
-
logger.debug(
|
|
230
|
+
logger.debug("GIT", "Local repository exists, skipping git operations");
|
|
178
231
|
return repoPath;
|
|
179
232
|
}
|
|
180
233
|
if (!isLocalRepo) {
|
|
181
|
-
logger.debug(
|
|
234
|
+
logger.debug("GIT", "Repository exists, performing update");
|
|
182
235
|
await this.updateRepository(tech.name, tech.group);
|
|
183
236
|
}
|
|
184
237
|
}
|
|
185
238
|
else {
|
|
186
239
|
if (isLocalRepo) {
|
|
187
|
-
logger.error(
|
|
240
|
+
logger.error("GIT", "Local repository path does not exist", undefined, {
|
|
241
|
+
repoName,
|
|
242
|
+
repoPath,
|
|
243
|
+
});
|
|
188
244
|
throw new Error(`Local repository ${repoName} does not exist at ${repoPath}`);
|
|
189
245
|
}
|
|
190
|
-
logger.debug(
|
|
246
|
+
logger.debug("GIT", "Repository does not exist, performing clone");
|
|
191
247
|
return await this.cloneRepository(tech.name, tech.repo, tech.group, tech.branch);
|
|
192
248
|
}
|
|
193
249
|
return repoPath;
|
|
194
250
|
}
|
|
195
|
-
async cloneRepository(repoName, repoUrl, groupName =
|
|
196
|
-
logger.info(
|
|
197
|
-
|
|
251
|
+
async cloneRepository(repoName, repoUrl, groupName = "default", branch = "main") {
|
|
252
|
+
logger.info("GIT", "Cloning repository", {
|
|
253
|
+
repoName,
|
|
254
|
+
repoHost: repoUrl.split("/")[2] || "unknown",
|
|
255
|
+
branch,
|
|
256
|
+
groupName,
|
|
257
|
+
});
|
|
258
|
+
const timingId = logger.timingStart("cloneRepository");
|
|
198
259
|
const repoPath = this.getSecureRepoPath(repoName, groupName);
|
|
199
260
|
if (fs.existsSync(repoPath)) {
|
|
200
|
-
const gitPath = path.join(repoPath,
|
|
261
|
+
const gitPath = path.join(repoPath, ".git");
|
|
201
262
|
if (fs.existsSync(gitPath)) {
|
|
202
|
-
logger.debug(
|
|
263
|
+
logger.debug("GIT", "Repository already exists as git repo, updating instead");
|
|
203
264
|
await this.updateRepository(repoName, groupName);
|
|
204
265
|
return repoPath;
|
|
205
266
|
}
|
|
206
267
|
}
|
|
207
268
|
const groupPath = this.getSecureGroupPath(groupName);
|
|
208
269
|
if (!fs.existsSync(groupPath)) {
|
|
209
|
-
logger.debug(
|
|
270
|
+
logger.debug("GIT", "Creating group directory", {
|
|
271
|
+
groupName,
|
|
272
|
+
path: groupPath.replace(os.homedir(), "~"),
|
|
273
|
+
});
|
|
210
274
|
fs.mkdirSync(groupPath, { recursive: true });
|
|
211
275
|
}
|
|
212
|
-
logger.debug(
|
|
276
|
+
logger.debug("GIT", "Starting shallow clone", { depth: 1 });
|
|
213
277
|
await clone({
|
|
214
278
|
fs,
|
|
215
279
|
http,
|
|
@@ -224,7 +288,7 @@ export class Librarian {
|
|
|
224
288
|
try {
|
|
225
289
|
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
226
290
|
for (const file of files) {
|
|
227
|
-
if (file.name ===
|
|
291
|
+
if (file.name === ".git") {
|
|
228
292
|
continue;
|
|
229
293
|
}
|
|
230
294
|
if (file.isDirectory()) {
|
|
@@ -239,16 +303,19 @@ export class Librarian {
|
|
|
239
303
|
}
|
|
240
304
|
};
|
|
241
305
|
countFiles(repoPath);
|
|
242
|
-
logger.debug(
|
|
243
|
-
logger.timingEnd(timingId,
|
|
306
|
+
logger.debug("GIT", "Clone completed", { fileCount });
|
|
307
|
+
logger.timingEnd(timingId, "GIT", `Repository cloned: ${repoName}`);
|
|
244
308
|
return repoPath;
|
|
245
309
|
}
|
|
246
310
|
async queryRepository(repoName, query) {
|
|
247
|
-
logger.info(
|
|
248
|
-
|
|
311
|
+
logger.info("LIBRARIAN", "Querying repository", {
|
|
312
|
+
repoName,
|
|
313
|
+
queryLength: query.length,
|
|
314
|
+
});
|
|
315
|
+
const timingId = logger.timingStart("queryRepository");
|
|
249
316
|
const tech = this.resolveTechnology(repoName);
|
|
250
317
|
if (!tech) {
|
|
251
|
-
logger.error(
|
|
318
|
+
logger.error("LIBRARIAN", "Technology not found in configuration", undefined, { repoName });
|
|
252
319
|
throw new Error(`Repository ${repoName} not found in configuration`);
|
|
253
320
|
}
|
|
254
321
|
const repoPath = await this.syncRepository(repoName);
|
|
@@ -257,10 +324,10 @@ export class Librarian {
|
|
|
257
324
|
group: tech.group,
|
|
258
325
|
technology: tech.name,
|
|
259
326
|
};
|
|
260
|
-
logger.debug(
|
|
261
|
-
workingDir: context.workingDir.replace(os.homedir(),
|
|
327
|
+
logger.debug("LIBRARIAN", "Initializing agent for query with context", {
|
|
328
|
+
workingDir: context.workingDir.replace(os.homedir(), "~"),
|
|
262
329
|
group: context.group,
|
|
263
|
-
technology: context.technology
|
|
330
|
+
technology: context.technology,
|
|
264
331
|
});
|
|
265
332
|
const agent = new ReactAgent({
|
|
266
333
|
aiProvider: this.config.aiProvider,
|
|
@@ -268,35 +335,43 @@ export class Librarian {
|
|
|
268
335
|
technology: {
|
|
269
336
|
name: tech.name,
|
|
270
337
|
repository: tech.repo,
|
|
271
|
-
branch: tech.branch
|
|
272
|
-
}
|
|
338
|
+
branch: tech.branch,
|
|
339
|
+
},
|
|
273
340
|
});
|
|
274
341
|
await agent.initialize();
|
|
275
342
|
const result = await agent.queryRepository(repoPath, query, context);
|
|
276
|
-
logger.timingEnd(timingId,
|
|
277
|
-
logger.info(
|
|
343
|
+
logger.timingEnd(timingId, "LIBRARIAN", `Query completed: ${repoName}`);
|
|
344
|
+
logger.info("LIBRARIAN", "Query result received", {
|
|
345
|
+
repoName,
|
|
346
|
+
responseLength: result.length,
|
|
347
|
+
});
|
|
278
348
|
return result;
|
|
279
349
|
}
|
|
280
350
|
async *streamRepository(repoName, query) {
|
|
281
|
-
logger.info(
|
|
282
|
-
|
|
351
|
+
logger.info("LIBRARIAN", "Streaming repository query", {
|
|
352
|
+
repoName,
|
|
353
|
+
queryLength: query.length,
|
|
354
|
+
});
|
|
355
|
+
const timingId = logger.timingStart("streamRepository");
|
|
283
356
|
const tech = this.resolveTechnology(repoName);
|
|
284
357
|
if (!tech) {
|
|
285
|
-
logger.error(
|
|
358
|
+
logger.error("LIBRARIAN", "Technology not found in configuration", undefined, { repoName });
|
|
286
359
|
throw new Error(`Repository ${repoName} not found in configuration`);
|
|
287
360
|
}
|
|
288
361
|
let isInterrupted = false;
|
|
289
362
|
const cleanup = () => {
|
|
290
363
|
isInterrupted = true;
|
|
291
364
|
};
|
|
292
|
-
logger.debug(
|
|
293
|
-
process.on(
|
|
294
|
-
process.on(
|
|
365
|
+
logger.debug("LIBRARIAN", "Setting up interruption handlers");
|
|
366
|
+
process.on("SIGINT", cleanup);
|
|
367
|
+
process.on("SIGTERM", cleanup);
|
|
295
368
|
try {
|
|
296
369
|
const repoPath = await this.syncRepository(repoName);
|
|
297
370
|
if (isInterrupted) {
|
|
298
|
-
logger.warn(
|
|
299
|
-
|
|
371
|
+
logger.warn("LIBRARIAN", "Repository sync interrupted by user", {
|
|
372
|
+
repoName,
|
|
373
|
+
});
|
|
374
|
+
yield "[Repository sync interrupted by user]";
|
|
300
375
|
return;
|
|
301
376
|
}
|
|
302
377
|
const context = {
|
|
@@ -304,10 +379,10 @@ export class Librarian {
|
|
|
304
379
|
group: tech.group,
|
|
305
380
|
technology: tech.name,
|
|
306
381
|
};
|
|
307
|
-
logger.debug(
|
|
308
|
-
workingDir: context.workingDir.replace(os.homedir(),
|
|
382
|
+
logger.debug("LIBRARIAN", "Initializing agent for streaming query with context", {
|
|
383
|
+
workingDir: context.workingDir.replace(os.homedir(), "~"),
|
|
309
384
|
group: context.group,
|
|
310
|
-
technology: context.technology
|
|
385
|
+
technology: context.technology,
|
|
311
386
|
});
|
|
312
387
|
const agent = new ReactAgent({
|
|
313
388
|
aiProvider: this.config.aiProvider,
|
|
@@ -315,56 +390,67 @@ export class Librarian {
|
|
|
315
390
|
technology: {
|
|
316
391
|
name: tech.name,
|
|
317
392
|
repository: tech.repo,
|
|
318
|
-
branch: tech.branch
|
|
319
|
-
}
|
|
393
|
+
branch: tech.branch,
|
|
394
|
+
},
|
|
320
395
|
});
|
|
321
396
|
await agent.initialize();
|
|
322
397
|
if (isInterrupted) {
|
|
323
|
-
logger.warn(
|
|
324
|
-
|
|
398
|
+
logger.warn("LIBRARIAN", "Agent initialization interrupted by user", {
|
|
399
|
+
repoName,
|
|
400
|
+
});
|
|
401
|
+
yield "[Agent initialization interrupted by user]";
|
|
325
402
|
return;
|
|
326
403
|
}
|
|
327
|
-
logger.debug(
|
|
404
|
+
logger.debug("LIBRARIAN", "Starting stream from agent");
|
|
328
405
|
yield* agent.streamRepository(repoPath, query, context);
|
|
329
406
|
}
|
|
330
407
|
catch (error) {
|
|
331
|
-
let errorMessage =
|
|
408
|
+
let errorMessage = "Unknown error";
|
|
332
409
|
if (error instanceof Error) {
|
|
333
|
-
if (error.message.includes(
|
|
410
|
+
if (error.message.includes("not found in configuration")) {
|
|
334
411
|
errorMessage = error.message;
|
|
335
412
|
}
|
|
336
|
-
else if (error.message.includes(
|
|
413
|
+
else if (error.message.includes("git") ||
|
|
414
|
+
error.message.includes("clone")) {
|
|
337
415
|
errorMessage = `Repository operation failed: ${error.message}`;
|
|
338
416
|
}
|
|
339
|
-
else if (error.message.includes(
|
|
340
|
-
errorMessage =
|
|
417
|
+
else if (error.message.includes("timeout")) {
|
|
418
|
+
errorMessage = "Repository operation timed out";
|
|
341
419
|
}
|
|
342
420
|
else {
|
|
343
421
|
errorMessage = `Repository error: ${error.message}`;
|
|
344
422
|
}
|
|
345
423
|
}
|
|
346
|
-
logger.error(
|
|
424
|
+
logger.error("LIBRARIAN", "Stream error", error instanceof Error ? error : new Error(errorMessage), { repoName });
|
|
347
425
|
yield `\n[Error: ${errorMessage}]`;
|
|
348
426
|
throw error;
|
|
349
427
|
}
|
|
350
428
|
finally {
|
|
351
|
-
process.removeListener(
|
|
352
|
-
process.removeListener(
|
|
353
|
-
logger.timingEnd(timingId,
|
|
429
|
+
process.removeListener("SIGINT", cleanup);
|
|
430
|
+
process.removeListener("SIGTERM", cleanup);
|
|
431
|
+
logger.timingEnd(timingId, "LIBRARIAN", `Stream completed: ${repoName}`);
|
|
354
432
|
}
|
|
355
433
|
}
|
|
356
434
|
async queryGroup(groupName, query) {
|
|
357
|
-
logger.info(
|
|
358
|
-
|
|
435
|
+
logger.info("LIBRARIAN", "Querying group", {
|
|
436
|
+
groupName,
|
|
437
|
+
queryLength: query.length,
|
|
438
|
+
});
|
|
439
|
+
const timingId = logger.timingStart("queryGroup");
|
|
359
440
|
if (!this.config.technologies[groupName]) {
|
|
360
|
-
logger.error(
|
|
441
|
+
logger.error("LIBRARIAN", "Group not found in configuration", undefined, {
|
|
442
|
+
groupName,
|
|
443
|
+
});
|
|
361
444
|
throw new Error(`Group ${groupName} not found in configuration`);
|
|
362
445
|
}
|
|
363
446
|
const groupPath = this.getSecureGroupPath(groupName);
|
|
364
447
|
const technologies = this.config.technologies[groupName];
|
|
365
448
|
if (technologies) {
|
|
366
449
|
const techNames = Object.keys(technologies);
|
|
367
|
-
logger.info(
|
|
450
|
+
logger.info("LIBRARIAN", "Syncing all technologies in group", {
|
|
451
|
+
groupName,
|
|
452
|
+
techCount: techNames.length,
|
|
453
|
+
});
|
|
368
454
|
for (const techName of techNames) {
|
|
369
455
|
await this.syncRepository(techName);
|
|
370
456
|
}
|
|
@@ -372,27 +458,35 @@ export class Librarian {
|
|
|
372
458
|
const context = {
|
|
373
459
|
workingDir: groupPath,
|
|
374
460
|
group: groupName,
|
|
375
|
-
technology:
|
|
461
|
+
technology: "",
|
|
376
462
|
};
|
|
377
|
-
logger.debug(
|
|
378
|
-
workingDir: context.workingDir.replace(os.homedir(),
|
|
379
|
-
group: context.group
|
|
463
|
+
logger.debug("LIBRARIAN", "Initializing agent for group query with context", {
|
|
464
|
+
workingDir: context.workingDir.replace(os.homedir(), "~"),
|
|
465
|
+
group: context.group,
|
|
380
466
|
});
|
|
381
467
|
const agent = new ReactAgent({
|
|
382
468
|
aiProvider: this.config.aiProvider,
|
|
383
|
-
workingDir: groupPath
|
|
469
|
+
workingDir: groupPath,
|
|
384
470
|
});
|
|
385
471
|
await agent.initialize();
|
|
386
472
|
const result = await agent.queryRepository(groupPath, query, context);
|
|
387
|
-
logger.timingEnd(timingId,
|
|
388
|
-
logger.info(
|
|
473
|
+
logger.timingEnd(timingId, "LIBRARIAN", `Group query completed: ${groupName}`);
|
|
474
|
+
logger.info("LIBRARIAN", "Group query result received", {
|
|
475
|
+
groupName,
|
|
476
|
+
responseLength: result.length,
|
|
477
|
+
});
|
|
389
478
|
return result;
|
|
390
479
|
}
|
|
391
480
|
async *streamGroup(groupName, query) {
|
|
392
|
-
logger.info(
|
|
393
|
-
|
|
481
|
+
logger.info("LIBRARIAN", "Streaming group query", {
|
|
482
|
+
groupName,
|
|
483
|
+
queryLength: query.length,
|
|
484
|
+
});
|
|
485
|
+
const timingId = logger.timingStart("streamGroup");
|
|
394
486
|
if (!this.config.technologies[groupName]) {
|
|
395
|
-
logger.error(
|
|
487
|
+
logger.error("LIBRARIAN", "Group not found in configuration", undefined, {
|
|
488
|
+
groupName,
|
|
489
|
+
});
|
|
396
490
|
throw new Error(`Group ${groupName} not found in configuration`);
|
|
397
491
|
}
|
|
398
492
|
const groupPath = this.getSecureGroupPath(groupName);
|
|
@@ -400,69 +494,71 @@ export class Librarian {
|
|
|
400
494
|
const cleanup = () => {
|
|
401
495
|
isInterrupted = true;
|
|
402
496
|
};
|
|
403
|
-
logger.debug(
|
|
404
|
-
process.on(
|
|
405
|
-
process.on(
|
|
497
|
+
logger.debug("LIBRARIAN", "Setting up interruption handlers for group");
|
|
498
|
+
process.on("SIGINT", cleanup);
|
|
499
|
+
process.on("SIGTERM", cleanup);
|
|
406
500
|
try {
|
|
407
501
|
const technologies = this.config.technologies[groupName];
|
|
408
502
|
if (technologies) {
|
|
409
503
|
const techNames = Object.keys(technologies);
|
|
410
|
-
logger.info(
|
|
504
|
+
logger.info("LIBRARIAN", "Syncing all technologies in group for streaming", { groupName, techCount: techNames.length });
|
|
411
505
|
for (const techName of techNames) {
|
|
412
506
|
await this.syncRepository(techName);
|
|
413
507
|
}
|
|
414
508
|
}
|
|
415
509
|
if (isInterrupted) {
|
|
416
|
-
logger.warn(
|
|
417
|
-
|
|
510
|
+
logger.warn("LIBRARIAN", "Group sync interrupted by user", {
|
|
511
|
+
groupName,
|
|
512
|
+
});
|
|
513
|
+
yield "[Repository sync interrupted by user]";
|
|
418
514
|
return;
|
|
419
515
|
}
|
|
420
516
|
const context = {
|
|
421
517
|
workingDir: groupPath,
|
|
422
518
|
group: groupName,
|
|
423
|
-
technology:
|
|
519
|
+
technology: "",
|
|
424
520
|
};
|
|
425
|
-
logger.debug(
|
|
426
|
-
workingDir: context.workingDir.replace(os.homedir(),
|
|
427
|
-
group: context.group
|
|
521
|
+
logger.debug("LIBRARIAN", "Initializing agent for group streaming with context", {
|
|
522
|
+
workingDir: context.workingDir.replace(os.homedir(), "~"),
|
|
523
|
+
group: context.group,
|
|
428
524
|
});
|
|
429
525
|
const agent = new ReactAgent({
|
|
430
526
|
aiProvider: this.config.aiProvider,
|
|
431
|
-
workingDir: groupPath
|
|
527
|
+
workingDir: groupPath,
|
|
432
528
|
});
|
|
433
529
|
await agent.initialize();
|
|
434
530
|
if (isInterrupted) {
|
|
435
|
-
logger.warn(
|
|
436
|
-
yield
|
|
531
|
+
logger.warn("LIBRARIAN", "Agent initialization interrupted by user for group", { groupName });
|
|
532
|
+
yield "[Agent initialization interrupted by user]";
|
|
437
533
|
return;
|
|
438
534
|
}
|
|
439
|
-
logger.debug(
|
|
535
|
+
logger.debug("LIBRARIAN", "Starting stream from agent for group");
|
|
440
536
|
yield* agent.streamRepository(groupPath, query, context);
|
|
441
537
|
}
|
|
442
538
|
catch (error) {
|
|
443
539
|
const errorMessage = this.getGroupStreamErrorMessage(error);
|
|
444
|
-
logger.error(
|
|
540
|
+
logger.error("LIBRARIAN", "Group stream error", error instanceof Error ? error : new Error(errorMessage), { groupName });
|
|
445
541
|
yield `\n[Error: ${errorMessage}]`;
|
|
446
542
|
throw error;
|
|
447
543
|
}
|
|
448
544
|
finally {
|
|
449
|
-
process.removeListener(
|
|
450
|
-
process.removeListener(
|
|
451
|
-
logger.timingEnd(timingId,
|
|
545
|
+
process.removeListener("SIGINT", cleanup);
|
|
546
|
+
process.removeListener("SIGTERM", cleanup);
|
|
547
|
+
logger.timingEnd(timingId, "LIBRARIAN", `Group stream completed: ${groupName}`);
|
|
452
548
|
}
|
|
453
549
|
}
|
|
454
550
|
getGroupStreamErrorMessage(error) {
|
|
455
551
|
if (!(error instanceof Error)) {
|
|
456
|
-
return
|
|
552
|
+
return "Unknown error";
|
|
457
553
|
}
|
|
458
|
-
if (error.message.includes(
|
|
554
|
+
if (error.message.includes("not found in configuration")) {
|
|
459
555
|
return error.message;
|
|
460
556
|
}
|
|
461
|
-
if (error.message.includes(
|
|
557
|
+
if (error.message.includes("git") || error.message.includes("clone")) {
|
|
462
558
|
return `Repository operation failed: ${error.message}`;
|
|
463
559
|
}
|
|
464
|
-
if (error.message.includes(
|
|
465
|
-
return
|
|
560
|
+
if (error.message.includes("timeout")) {
|
|
561
|
+
return "Repository operation timed out";
|
|
466
562
|
}
|
|
467
563
|
return `Group error: ${error.message}`;
|
|
468
564
|
}
|