kennel 0.2.31 → 0.2.32
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/dist/cli/dispatch.js +5 -0
- package/dist/cli/usage.js +4 -0
- package/dist/core/runtime.js +96 -54
- package/dist/llm/clipboard.d.ts +1 -0
- package/dist/llm/clipboard.js +59 -0
- package/dist/llm/command.d.ts +1 -0
- package/dist/llm/command.js +219 -0
- package/dist/llm/worker.d.ts +19 -0
- package/dist/llm/worker.js +866 -0
- package/dist/package/preview.js +98 -56
- package/dist/plugins/external.d.ts +6 -0
- package/dist/plugins/external.js +16 -0
- package/dist/site/server-plugins.d.ts +12 -0
- package/dist/site/server-plugins.js +82 -2
- package/package.json +3 -2
- package/templates/plugin/package.json +1 -0
- package/templates/plugin/server.ts +43 -0
- package/templates/plugin/tsconfig.json +1 -1
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
var __esm = (fn, res, err) => () => {
|
|
2
|
+
if (fn)
|
|
3
|
+
try {
|
|
4
|
+
res = fn(fn = 0);
|
|
5
|
+
} catch (e) {
|
|
6
|
+
err = [e];
|
|
7
|
+
}
|
|
8
|
+
if (err)
|
|
9
|
+
throw err[0];
|
|
10
|
+
return res;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// packages/llm/src/context.ts
|
|
14
|
+
import {
|
|
15
|
+
closeSync as closeSync2,
|
|
16
|
+
constants as constants2,
|
|
17
|
+
fstatSync as fstatSync2,
|
|
18
|
+
openSync as openSync2,
|
|
19
|
+
readSync as readSync2,
|
|
20
|
+
statSync as statSync3
|
|
21
|
+
} from "node:fs";
|
|
22
|
+
import { dirname as dirname3, join as join3, relative as relative2 } from "node:path";
|
|
23
|
+
import local3 from "@raggle-ai/local";
|
|
24
|
+
function unique(values) {
|
|
25
|
+
return [...new Set(values)];
|
|
26
|
+
}
|
|
27
|
+
function singleLine(text) {
|
|
28
|
+
return text.replace(/\s+/g, " ").trim();
|
|
29
|
+
}
|
|
30
|
+
function gitRoot2(folder) {
|
|
31
|
+
for (let directory = folder;; directory = dirname3(directory)) {
|
|
32
|
+
if (statSync3(join3(directory, ".git"), { throwIfNoEntry: false })) {
|
|
33
|
+
return directory;
|
|
34
|
+
}
|
|
35
|
+
if (dirname3(directory) === directory)
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function projectConfigDirectory(folder) {
|
|
40
|
+
const boundary = gitRoot2(folder) ?? folder;
|
|
41
|
+
for (let directory = folder;; directory = dirname3(directory)) {
|
|
42
|
+
const config = statSync3(join3(directory, "raggle.json"), {
|
|
43
|
+
throwIfNoEntry: false
|
|
44
|
+
});
|
|
45
|
+
if (config?.isFile())
|
|
46
|
+
return directory;
|
|
47
|
+
if (directory === boundary || dirname3(directory) === directory) {
|
|
48
|
+
return boundary;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function readProjectConfig(directory) {
|
|
53
|
+
try {
|
|
54
|
+
return local3.readRaggleProjectConfig(directory);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error(`Invalid Raggle config ${join3(directory, "raggle.json")}: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function readReadmePrefix(worktree) {
|
|
60
|
+
for (const name of readmeNames) {
|
|
61
|
+
let fd;
|
|
62
|
+
try {
|
|
63
|
+
fd = openSync2(join3(worktree, name), constants2.O_RDONLY | (constants2.O_NONBLOCK ?? 0));
|
|
64
|
+
if (!fstatSync2(fd).isFile())
|
|
65
|
+
continue;
|
|
66
|
+
const buffer = Buffer.alloc(maxReadmeBytes);
|
|
67
|
+
const bytes = readSync2(fd, buffer, 0, buffer.length, 0);
|
|
68
|
+
return buffer.toString("utf8", 0, bytes);
|
|
69
|
+
} catch {} finally {
|
|
70
|
+
if (fd !== undefined)
|
|
71
|
+
closeSync2(fd);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
function firstUsefulParagraph(markdown) {
|
|
77
|
+
const paragraphs = markdown.replace(/^---\n[\s\S]*?\n---\n/, "").split(/\n\s*\n/);
|
|
78
|
+
for (const paragraph of paragraphs) {
|
|
79
|
+
const text = paragraph.split(`
|
|
80
|
+
`).map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && !line.startsWith("```") && !line.startsWith("![") && !line.startsWith("[") && !line.startsWith("<") && !line.startsWith("|") && !/^[-:| ]+$/.test(line)).join(" ").replace(/!\[[^\]]*\]\([^)]*\)/g, "").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/[*_`]/g, "").replace(/\s+/g, " ").trim();
|
|
81
|
+
if (text.length < 20)
|
|
82
|
+
continue;
|
|
83
|
+
return text.length > maxSummaryLength ? `${text.slice(0, maxSummaryLength - 1).trimEnd()}…` : text;
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
function projectSummary(project) {
|
|
88
|
+
const description = singleLine(project.description ?? "");
|
|
89
|
+
if (description)
|
|
90
|
+
return description;
|
|
91
|
+
const readme = readReadmePrefix(project.repositoryRoot ?? project.worktree);
|
|
92
|
+
return readme ? firstUsefulParagraph(readme) : undefined;
|
|
93
|
+
}
|
|
94
|
+
function repositoryLabel(project) {
|
|
95
|
+
const remoteUrl = project.remoteMismatch?.actualRemoteUrl ?? project.remoteUrl;
|
|
96
|
+
const browserUrl = remoteUrl ? local3.remoteToBrowserUrl(remoteUrl) ?? remoteUrl : undefined;
|
|
97
|
+
const worktree = canonicalPath(project.worktree);
|
|
98
|
+
return browserUrl ? `${worktree} (${browserUrl})` : worktree;
|
|
99
|
+
}
|
|
100
|
+
function repositoryKeys(project) {
|
|
101
|
+
return [
|
|
102
|
+
project.remoteMismatch?.actualRemoteUrl,
|
|
103
|
+
project.remoteUrl,
|
|
104
|
+
...project.relatedIds ?? []
|
|
105
|
+
].flatMap((url) => {
|
|
106
|
+
const key = url ? local3.repositoryLocationKey(url) : undefined;
|
|
107
|
+
return key ? [key] : [];
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function isOrganisationKey(key) {
|
|
111
|
+
const parts = key.split("/");
|
|
112
|
+
return parts.length === 2 && (parts[0] === "github.com" || Boolean(parts[0]?.endsWith(".github.com")));
|
|
113
|
+
}
|
|
114
|
+
function missingLinks(config, connected) {
|
|
115
|
+
const selected = canonicalPath(connected.selectedWorktree);
|
|
116
|
+
const direct = config.linkedRepositories.filter((entry) => canonicalPath(entry.project) === selected);
|
|
117
|
+
if (!direct.length)
|
|
118
|
+
return [];
|
|
119
|
+
const present = new Set([...connected.linked, ...connected.linking, ...connected.local].flatMap(repositoryKeys));
|
|
120
|
+
return direct.filter(({ repository }) => {
|
|
121
|
+
const key = local3.repositoryLocationKey(repository);
|
|
122
|
+
return Boolean(key) && !isOrganisationKey(key) && !present.has(key);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function relationHeading(type) {
|
|
126
|
+
if (type === "notes")
|
|
127
|
+
return "Non-code project material";
|
|
128
|
+
if (type === "implementation")
|
|
129
|
+
return "Implementations";
|
|
130
|
+
if (type === "dependency")
|
|
131
|
+
return "Dependencies";
|
|
132
|
+
return "Linked folders";
|
|
133
|
+
}
|
|
134
|
+
function treeLines(root, projects, relations, direction, type, reasons) {
|
|
135
|
+
const projectByWorktree = new Map(projects.map((project) => [canonicalPath(project.worktree), project]));
|
|
136
|
+
const children = new Map;
|
|
137
|
+
for (const relation of relations) {
|
|
138
|
+
const parent = direction === "linked" ? relation.source : relation.target;
|
|
139
|
+
const child = direction === "linked" ? relation.target : relation.source;
|
|
140
|
+
if (!projectByWorktree.has(child))
|
|
141
|
+
continue;
|
|
142
|
+
const items = children.get(parent) ?? [];
|
|
143
|
+
if (!items.some((item) => item.source === relation.source && item.target === relation.target && item.type === relation.type)) {
|
|
144
|
+
items.push(relation);
|
|
145
|
+
}
|
|
146
|
+
children.set(parent, items);
|
|
147
|
+
}
|
|
148
|
+
const lines = [];
|
|
149
|
+
const visited = new Set;
|
|
150
|
+
const append = (parent, indent, allowedType, parentType) => {
|
|
151
|
+
const byType = new Map;
|
|
152
|
+
for (const relation of children.get(parent) ?? []) {
|
|
153
|
+
if (allowedType && relation.type !== allowedType)
|
|
154
|
+
continue;
|
|
155
|
+
byType.set(relation.type, [
|
|
156
|
+
...byType.get(relation.type) ?? [],
|
|
157
|
+
relation
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
for (const [relationType, childRelations] of byType) {
|
|
161
|
+
const showHeading = !allowedType && relationType !== parentType;
|
|
162
|
+
if (showHeading) {
|
|
163
|
+
lines.push(`${" ".repeat(indent)}${relationHeading(relationType)}:`);
|
|
164
|
+
}
|
|
165
|
+
const childIndent = showHeading ? indent + 1 : indent;
|
|
166
|
+
const detail = " ".repeat(childIndent + 1);
|
|
167
|
+
for (const relation of childRelations) {
|
|
168
|
+
const child = direction === "linked" ? relation.target : relation.source;
|
|
169
|
+
if (visited.has(child))
|
|
170
|
+
continue;
|
|
171
|
+
visited.add(child);
|
|
172
|
+
const project = projectByWorktree.get(child);
|
|
173
|
+
if (!project)
|
|
174
|
+
continue;
|
|
175
|
+
lines.push(`${" ".repeat(childIndent)}- ${repositoryLabel(project)}`);
|
|
176
|
+
const summary = projectSummary(project);
|
|
177
|
+
if (summary)
|
|
178
|
+
lines.push(`${detail}Description: ${summary}`);
|
|
179
|
+
const reason = singleLine(reasons.get(child) ?? "");
|
|
180
|
+
if (reason)
|
|
181
|
+
lines.push(`${detail}Relation note: ${reason}`);
|
|
182
|
+
append(child, childIndent + 1, undefined, relationType);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
append(canonicalPath(root), 0, type);
|
|
187
|
+
return lines;
|
|
188
|
+
}
|
|
189
|
+
function localSubpathName(folder, project) {
|
|
190
|
+
return project.relativePath || relative2(canonicalPath(folder), canonicalPath(project.worktree));
|
|
191
|
+
}
|
|
192
|
+
function modeLabel(depth) {
|
|
193
|
+
if (depth === "full")
|
|
194
|
+
return "full (all reachable links)";
|
|
195
|
+
return `expanded to depth ${depth}`;
|
|
196
|
+
}
|
|
197
|
+
async function buildCoreContext(options) {
|
|
198
|
+
const { folder, depth, signal } = options;
|
|
199
|
+
const configDirectory = projectConfigDirectory(folder);
|
|
200
|
+
const config = readProjectConfig(configDirectory);
|
|
201
|
+
const connected = await local3.mergePersonalRelations(folder, await local3.listConnectedProjects(folder, {
|
|
202
|
+
depth
|
|
203
|
+
}));
|
|
204
|
+
signal.throwIfAborted();
|
|
205
|
+
const selected = canonicalPath(connected.selectedWorktree);
|
|
206
|
+
const reasons = connected.personalReasons;
|
|
207
|
+
const tree = (projects, direction, type) => treeLines(selected, projects, connected.relations, direction, type, reasons);
|
|
208
|
+
const related = [
|
|
209
|
+
...tree(connected.linked, "linked", "related"),
|
|
210
|
+
...tree(connected.linking, "linking", "related")
|
|
211
|
+
];
|
|
212
|
+
const implementations = [
|
|
213
|
+
...tree(connected.linked, "linked", "implementation"),
|
|
214
|
+
...tree(connected.linking, "linking", "implementation"),
|
|
215
|
+
...tree(connected.linked, "linked", "notes")
|
|
216
|
+
];
|
|
217
|
+
const dependencies = tree(connected.linked, "linked", "dependency");
|
|
218
|
+
const dependents = tree(connected.linking, "linking", "dependency");
|
|
219
|
+
const linkedKeys = new Set(connected.linked.map((project) => canonicalPath(project.worktree)));
|
|
220
|
+
const localSubpaths = connected.local.filter((project) => !linkedKeys.has(canonicalPath(project.worktree))).map((project) => localSubpathName(folder, project)).filter((name) => name && name !== ".");
|
|
221
|
+
const missing = missingLinks(config, connected);
|
|
222
|
+
const configFile = join3(configDirectory, "raggle.json");
|
|
223
|
+
const blocks = [
|
|
224
|
+
[
|
|
225
|
+
"## Folder",
|
|
226
|
+
"",
|
|
227
|
+
`- Path: ${folder}`,
|
|
228
|
+
...selected !== folder ? [`- Raggle project: ${selected}`] : [],
|
|
229
|
+
...statSync3(configFile, { throwIfNoEntry: false })?.isFile() ? [`- Raggle config: ${configFile}`] : [],
|
|
230
|
+
`- Context: ${modeLabel(depth)}`
|
|
231
|
+
].join(`
|
|
232
|
+
`)
|
|
233
|
+
];
|
|
234
|
+
const groups = [
|
|
235
|
+
["Linked folders", related],
|
|
236
|
+
["Implementations", implementations],
|
|
237
|
+
["Dependencies", dependencies],
|
|
238
|
+
["Dependents", dependents],
|
|
239
|
+
["Key local subpaths", localSubpaths.map((name) => `- ${name}`)]
|
|
240
|
+
];
|
|
241
|
+
const hasLocations = groups.some(([, lines]) => lines.length > 0);
|
|
242
|
+
if (hasLocations) {
|
|
243
|
+
blocks.push([
|
|
244
|
+
"## Local code locations",
|
|
245
|
+
"",
|
|
246
|
+
"These repositories are already resolved to local checkouts. Use the paths below directly. Do not search for another checkout or clone these repositories again.",
|
|
247
|
+
...groups.flatMap(([label, lines]) => lines.length ? ["", `${label}:`, "", ...lines] : [])
|
|
248
|
+
].join(`
|
|
249
|
+
`));
|
|
250
|
+
blocks.push([
|
|
251
|
+
"## Working with this code",
|
|
252
|
+
"",
|
|
253
|
+
`- Read and search within the listed roots using absolute paths. Local subpaths are relative to ${folder}.`,
|
|
254
|
+
"- Read each repository's applicable agent instructions before editing.",
|
|
255
|
+
"- Edit files at their actual paths, not through .rgl/links mirrors.",
|
|
256
|
+
'- Run tests and Git commands in the repository being changed (for example: git -C "/absolute/repository/path" status).',
|
|
257
|
+
"- If a path is missing or access is denied, report it; do not silently choose another checkout."
|
|
258
|
+
].join(`
|
|
259
|
+
`));
|
|
260
|
+
}
|
|
261
|
+
if (missing.length) {
|
|
262
|
+
blocks.push([
|
|
263
|
+
"## Not available locally",
|
|
264
|
+
"",
|
|
265
|
+
...missing.map(({ repository, type }) => `- ${repository} (${type}): no local checkout resolved. Ask before cloning or selecting a different location.`)
|
|
266
|
+
].join(`
|
|
267
|
+
`));
|
|
268
|
+
}
|
|
269
|
+
if (!hasLocations && !missing.length) {
|
|
270
|
+
blocks.push([
|
|
271
|
+
"## Links",
|
|
272
|
+
"",
|
|
273
|
+
`No linked repositories, notes folders, or subprojects were found for ${folder}.`,
|
|
274
|
+
"Declare related repositories in the project's raggle.json to add their local paths and descriptions to this context."
|
|
275
|
+
].join(`
|
|
276
|
+
`));
|
|
277
|
+
}
|
|
278
|
+
const linkedFolders = unique(connected.relations.filter((relation) => relation.type === "notes" && relation.origin !== "organisation" && (canonicalPath(relation.target) === selected || canonicalPath(relation.source) === selected)).map((relation) => canonicalPath(relation.target) === selected ? canonicalPath(relation.source) : canonicalPath(relation.target))).filter((path) => path !== folder && Boolean(statSync3(path, { throwIfNoEntry: false })?.isDirectory()));
|
|
279
|
+
return { blocks, linkedFolders };
|
|
280
|
+
}
|
|
281
|
+
var canonicalPath, readmeNames, maxReadmeBytes, maxSummaryLength = 280;
|
|
282
|
+
var init_context = __esm(() => {
|
|
283
|
+
({ canonicalPath } = local3);
|
|
284
|
+
readmeNames = ["README.md", "README.markdown", "README", "readme.md"];
|
|
285
|
+
maxReadmeBytes = 16 * 1024;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// packages/llm/src/plugins.ts
|
|
289
|
+
function message(error) {
|
|
290
|
+
return error instanceof Error ? error.message : String(error);
|
|
291
|
+
}
|
|
292
|
+
function checkLines(name, lines, fail) {
|
|
293
|
+
let fence;
|
|
294
|
+
for (const [index, line] of lines.entries()) {
|
|
295
|
+
const where = `${name} line ${index + 1}`;
|
|
296
|
+
if (CONTROL.test(line)) {
|
|
297
|
+
fail(`${where} contains a line break or control character`);
|
|
298
|
+
}
|
|
299
|
+
const marker = FENCE.exec(line)?.[1];
|
|
300
|
+
if (fence) {
|
|
301
|
+
if (marker && marker[0] === fence[0] && marker.length >= fence.length && line.trim() === marker) {
|
|
302
|
+
fence = undefined;
|
|
303
|
+
}
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (marker) {
|
|
307
|
+
fence = marker;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (HEADING.test(line))
|
|
311
|
+
fail(`${where} is a heading; the host owns headings`);
|
|
312
|
+
if (SETEXT_UNDERLINE.test(line) && index > 0 && lines[index - 1]?.trim()) {
|
|
313
|
+
fail(`${where} would make the line before it a heading`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (fence)
|
|
317
|
+
fail(`${name} leaves a code fence open`);
|
|
318
|
+
}
|
|
319
|
+
function validateSections(plugin, output) {
|
|
320
|
+
const fail = (detail) => {
|
|
321
|
+
throw new Error(`Plugin ${plugin} returned invalid LLM context: ${detail}`);
|
|
322
|
+
};
|
|
323
|
+
if (!Array.isArray(output))
|
|
324
|
+
fail("expected an array of sections");
|
|
325
|
+
const ids = new Set;
|
|
326
|
+
const sections = [];
|
|
327
|
+
for (const [index, value] of output.entries()) {
|
|
328
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
329
|
+
fail(`section ${index + 1} is not an object`);
|
|
330
|
+
}
|
|
331
|
+
const { id, title, lines } = value;
|
|
332
|
+
if (typeof id !== "string" || !SECTION_ID.test(id)) {
|
|
333
|
+
fail(`section ${index + 1} needs an id of lowercase letters, digits, "-", or "_"`);
|
|
334
|
+
}
|
|
335
|
+
const name = `section ${plugin}/${id}`;
|
|
336
|
+
if (ids.has(id))
|
|
337
|
+
fail(`${name} is duplicated`);
|
|
338
|
+
ids.add(id);
|
|
339
|
+
if (typeof title !== "string" || !title.trim()) {
|
|
340
|
+
fail(`${name} needs a non-blank title`);
|
|
341
|
+
}
|
|
342
|
+
if (CONTROL.test(title)) {
|
|
343
|
+
fail(`${name} title contains a line break or control character`);
|
|
344
|
+
}
|
|
345
|
+
if (!Array.isArray(lines) || !lines.every((line) => typeof line === "string")) {
|
|
346
|
+
fail(`${name} lines must be an array of strings`);
|
|
347
|
+
}
|
|
348
|
+
const text = lines.map((line) => line.trim() ? line : "");
|
|
349
|
+
checkLines(name, text, fail);
|
|
350
|
+
const first = text.findIndex(Boolean);
|
|
351
|
+
if (first === -1)
|
|
352
|
+
continue;
|
|
353
|
+
let end = text.length;
|
|
354
|
+
while (!text[end - 1])
|
|
355
|
+
end -= 1;
|
|
356
|
+
sections.push({
|
|
357
|
+
id,
|
|
358
|
+
title: title.trim(),
|
|
359
|
+
lines: text.slice(first, end)
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return sections;
|
|
363
|
+
}
|
|
364
|
+
async function runContributors(contributors, context) {
|
|
365
|
+
const contributions = [];
|
|
366
|
+
const seen = new Set;
|
|
367
|
+
for (const contributor of contributors) {
|
|
368
|
+
if (seen.has(contributor.id)) {
|
|
369
|
+
throw new Error(`Duplicate plugin id ${contributor.id}`);
|
|
370
|
+
}
|
|
371
|
+
seen.add(contributor.id);
|
|
372
|
+
const label = typeof contributor.label === "string" ? contributor.label.trim() : "";
|
|
373
|
+
if (!label || CONTROL.test(label)) {
|
|
374
|
+
throw new Error(`Plugin ${contributor.id} has an invalid label`);
|
|
375
|
+
}
|
|
376
|
+
context.signal.throwIfAborted();
|
|
377
|
+
let output;
|
|
378
|
+
try {
|
|
379
|
+
output = await contributor.llm({
|
|
380
|
+
folder: context.folder,
|
|
381
|
+
linkedFolders: [...context.linkedFolders],
|
|
382
|
+
signal: context.signal
|
|
383
|
+
});
|
|
384
|
+
} catch (error) {
|
|
385
|
+
throw new Error(`Plugin ${contributor.id} failed to provide LLM context: ${message(error)}`);
|
|
386
|
+
}
|
|
387
|
+
context.signal.throwIfAborted();
|
|
388
|
+
const sections = validateSections(contributor.id, output);
|
|
389
|
+
if (sections.length) {
|
|
390
|
+
contributions.push({ id: contributor.id, label, sections });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return contributions;
|
|
394
|
+
}
|
|
395
|
+
var SECTION_ID, CONTROL, HEADING, SETEXT_UNDERLINE, FENCE;
|
|
396
|
+
var init_plugins = __esm(() => {
|
|
397
|
+
SECTION_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/u;
|
|
398
|
+
CONTROL = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u;
|
|
399
|
+
HEADING = /^ {0,3}#{1,6}(?: |$)/u;
|
|
400
|
+
SETEXT_UNDERLINE = /^ {0,3}(?:=+|-+) *$/u;
|
|
401
|
+
FENCE = /^ {0,3}(`{3,}|~{3,})/u;
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// packages/llm/src/index.ts
|
|
405
|
+
import { realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
|
|
406
|
+
|
|
407
|
+
// packages/llm/src/agents.ts
|
|
408
|
+
import { statSync as statSync2 } from "node:fs";
|
|
409
|
+
import { dirname as dirname2, isAbsolute, join as join2, relative, sep } from "node:path";
|
|
410
|
+
import local2 from "@raggle-ai/local";
|
|
411
|
+
|
|
412
|
+
// packages/llm/src/instructions.ts
|
|
413
|
+
import {
|
|
414
|
+
closeSync,
|
|
415
|
+
constants,
|
|
416
|
+
fstatSync,
|
|
417
|
+
lstatSync,
|
|
418
|
+
openSync,
|
|
419
|
+
readSync,
|
|
420
|
+
realpathSync,
|
|
421
|
+
statSync
|
|
422
|
+
} from "node:fs";
|
|
423
|
+
import { dirname, join } from "node:path";
|
|
424
|
+
import { TextDecoder } from "node:util";
|
|
425
|
+
import local from "@raggle-ai/local";
|
|
426
|
+
function readInstructionSettings() {
|
|
427
|
+
return local.readAgentInstructionSettings();
|
|
428
|
+
}
|
|
429
|
+
function assertProfileName(profile) {
|
|
430
|
+
if (!local.agentProfileSchema.safeParse(profile).success) {
|
|
431
|
+
throw new Error(`Invalid profile "${profile}": use lowercase letters, digits, hyphens, or underscores (at most 64 characters)`);
|
|
432
|
+
}
|
|
433
|
+
return profile;
|
|
434
|
+
}
|
|
435
|
+
function missing(error) {
|
|
436
|
+
return error.code === "ENOENT";
|
|
437
|
+
}
|
|
438
|
+
function checkedPath(base, segments) {
|
|
439
|
+
let current = base;
|
|
440
|
+
for (const [index, segment] of segments.entries()) {
|
|
441
|
+
current = join(current, segment);
|
|
442
|
+
const directory = index < segments.length - 1;
|
|
443
|
+
const info = lstatSync(current, { throwIfNoEntry: false });
|
|
444
|
+
if (info?.isSymbolicLink()) {
|
|
445
|
+
throw new Error(`Refusing instruction symlink: ${current}`);
|
|
446
|
+
}
|
|
447
|
+
if (info && (directory ? !info.isDirectory() : !info.isFile())) {
|
|
448
|
+
throw new Error(`Expected an instruction ${directory ? "directory" : "file"}: ${current}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return current;
|
|
452
|
+
}
|
|
453
|
+
function readInstructionFile(file, maxBytes) {
|
|
454
|
+
let fd;
|
|
455
|
+
try {
|
|
456
|
+
fd = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (missing(error))
|
|
459
|
+
return;
|
|
460
|
+
throw new Error(`Cannot read instructions at ${file}: ${error.message}`);
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const tooLarge = () => new Error(`Instructions exceed the ${maxBytes}-byte remaining budget: ${file}. Shorten the file or raise agentInstructions.maxBytes in the Raggle settings.`);
|
|
464
|
+
if (!fstatSync(fd).isFile()) {
|
|
465
|
+
throw new Error(`Not an instruction file: ${file}`);
|
|
466
|
+
}
|
|
467
|
+
if (fstatSync(fd).size > maxBytes)
|
|
468
|
+
throw tooLarge();
|
|
469
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
470
|
+
let bytes = 0;
|
|
471
|
+
while (bytes < buffer.length) {
|
|
472
|
+
const count = readSync(fd, buffer, bytes, buffer.length - bytes, null);
|
|
473
|
+
if (!count)
|
|
474
|
+
break;
|
|
475
|
+
bytes += count;
|
|
476
|
+
}
|
|
477
|
+
if (bytes > maxBytes)
|
|
478
|
+
throw tooLarge();
|
|
479
|
+
let content;
|
|
480
|
+
try {
|
|
481
|
+
content = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, bytes));
|
|
482
|
+
} catch {
|
|
483
|
+
throw new Error(`Instructions must be UTF-8 text: ${file}`);
|
|
484
|
+
}
|
|
485
|
+
if (content.includes("\x00")) {
|
|
486
|
+
throw new Error(`Instructions contain NUL bytes: ${file}`);
|
|
487
|
+
}
|
|
488
|
+
return { path: file, content, bytes };
|
|
489
|
+
} finally {
|
|
490
|
+
closeSync(fd);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function readRepositoryInstructions(folder, maxBytes) {
|
|
494
|
+
for (const name of ["AGENTS.md", "agents.md"]) {
|
|
495
|
+
const read = readInstructionFile(checkedPath(folder, [name]), maxBytes);
|
|
496
|
+
if (read)
|
|
497
|
+
return read;
|
|
498
|
+
}
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
function instructionProjectRoot(folder) {
|
|
502
|
+
const start = realpathSync(folder);
|
|
503
|
+
let configRoot;
|
|
504
|
+
for (let directory = start;; directory = dirname(directory)) {
|
|
505
|
+
const config = statSync(join(directory, "raggle.json"), {
|
|
506
|
+
throwIfNoEntry: false
|
|
507
|
+
});
|
|
508
|
+
if (!configRoot && config?.isFile())
|
|
509
|
+
configRoot = directory;
|
|
510
|
+
if (lstatSync(join(directory, ".git"), { throwIfNoEntry: false })) {
|
|
511
|
+
return configRoot ?? directory;
|
|
512
|
+
}
|
|
513
|
+
if (dirname(directory) === directory)
|
|
514
|
+
return configRoot ?? start;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function readPersonalInstructions(folder, settings, profile) {
|
|
518
|
+
const selected = assertProfileName(profile ?? settings.defaultProfile);
|
|
519
|
+
const root = instructionProjectRoot(folder);
|
|
520
|
+
const configDirectory = dirname(local.raggleUserSettingsPath());
|
|
521
|
+
const scopes = settings.includeGlobal ? ["global", "project"] : ["project"];
|
|
522
|
+
const sources = [];
|
|
523
|
+
let totalBytes = 0;
|
|
524
|
+
for (const scope of scopes) {
|
|
525
|
+
for (const name of new Set(["default", selected])) {
|
|
526
|
+
const file = scope === "global" ? checkedPath(configDirectory, ["agents", "instructions", `${name}.md`]) : checkedPath(root, [".rgl", "agent", `${name}.md`]);
|
|
527
|
+
const read = readInstructionFile(file, settings.maxBytes - totalBytes);
|
|
528
|
+
if (!read)
|
|
529
|
+
continue;
|
|
530
|
+
totalBytes += read.bytes;
|
|
531
|
+
const content = read.content.trim();
|
|
532
|
+
if (content) {
|
|
533
|
+
sources.push({ scope, profile: name, path: read.path, content });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return { root, sources };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// packages/llm/src/agents.ts
|
|
541
|
+
function exists(folder) {
|
|
542
|
+
return Boolean(statSync2(folder, { throwIfNoEntry: false })?.isDirectory());
|
|
543
|
+
}
|
|
544
|
+
function inside(folder, root) {
|
|
545
|
+
const path = relative(root, folder);
|
|
546
|
+
return path === "" || !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`);
|
|
547
|
+
}
|
|
548
|
+
function gitRoot(folder) {
|
|
549
|
+
for (let root = folder;; root = dirname2(root)) {
|
|
550
|
+
if (statSync2(join2(root, ".git"), { throwIfNoEntry: false }))
|
|
551
|
+
return root;
|
|
552
|
+
if (dirname2(root) === root)
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function configFolders(roots, boundary) {
|
|
557
|
+
const folders = new Set;
|
|
558
|
+
for (const root of roots) {
|
|
559
|
+
if (!exists(root))
|
|
560
|
+
continue;
|
|
561
|
+
const start = local2.canonicalPath(root);
|
|
562
|
+
for (let folder = start;; folder = dirname2(folder)) {
|
|
563
|
+
if (statSync2(join2(folder, "raggle.json"), {
|
|
564
|
+
throwIfNoEntry: false
|
|
565
|
+
})?.isFile()) {
|
|
566
|
+
folders.add(folder);
|
|
567
|
+
}
|
|
568
|
+
if (folder === boundary || dirname2(folder) === folder)
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return [...folders];
|
|
573
|
+
}
|
|
574
|
+
async function buildAgentContext(options) {
|
|
575
|
+
const { folder, signal } = options;
|
|
576
|
+
const settings = local2.readRaggleUserSettings();
|
|
577
|
+
const checkout = gitRoot(folder);
|
|
578
|
+
let remote;
|
|
579
|
+
if (checkout) {
|
|
580
|
+
try {
|
|
581
|
+
remote = await local2.gitRemoteUrl(checkout);
|
|
582
|
+
} catch (error) {
|
|
583
|
+
if (error.code !== 2)
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const identities = new Set;
|
|
588
|
+
const addIdentity = (repository) => {
|
|
589
|
+
const key = local2.repositoryLocationKey(repository);
|
|
590
|
+
if (key)
|
|
591
|
+
identities.add(key);
|
|
592
|
+
};
|
|
593
|
+
if (remote)
|
|
594
|
+
addIdentity(remote);
|
|
595
|
+
for (const [repository, location] of Object.entries(settings.repositoryLocations)) {
|
|
596
|
+
const preferred = local2.canonicalPath(location.preferredWorktree);
|
|
597
|
+
if (exists(preferred) && checkout && gitRoot(preferred) === checkout) {
|
|
598
|
+
addIdentity(repository);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const roots = [
|
|
602
|
+
folder,
|
|
603
|
+
...settings.projectDirectories,
|
|
604
|
+
...Object.values(settings.repositoryLocations).map((entry) => entry.preferredWorktree),
|
|
605
|
+
...local2.getResolvedWorkspaces().map((entry) => entry.worktree),
|
|
606
|
+
...settings.baseRepository ? [settings.baseRepository] : []
|
|
607
|
+
];
|
|
608
|
+
const organisations = new Set;
|
|
609
|
+
const notes = new Set;
|
|
610
|
+
const projectNotes = new Set;
|
|
611
|
+
const matches = (repository) => {
|
|
612
|
+
const key = local2.repositoryLocationKey(repository);
|
|
613
|
+
return Boolean(key && identities.has(key));
|
|
614
|
+
};
|
|
615
|
+
const configErrors = new Map;
|
|
616
|
+
for (const owner of configFolders(roots)) {
|
|
617
|
+
signal.throwIfAborted();
|
|
618
|
+
let config;
|
|
619
|
+
try {
|
|
620
|
+
config = local2.readRaggleProjectConfig(owner);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
configErrors.set(owner, error);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
const matchingOrganisations = config.linkedOrganisations.filter((entry) => {
|
|
626
|
+
const key = local2.repositoryLocationKey(entry.organisation);
|
|
627
|
+
return key && [...identities].some((identity) => identity.startsWith(`${key}/`));
|
|
628
|
+
});
|
|
629
|
+
for (const entry of matchingOrganisations) {
|
|
630
|
+
if (exists(entry.project))
|
|
631
|
+
organisations.add(local2.canonicalPath(entry.project));
|
|
632
|
+
}
|
|
633
|
+
for (const entry of config.linkedRepositories) {
|
|
634
|
+
if (!matches(entry.repository) || !exists(entry.project))
|
|
635
|
+
continue;
|
|
636
|
+
const source = local2.canonicalPath(entry.project);
|
|
637
|
+
if (entry.type === "notes")
|
|
638
|
+
notes.add(source);
|
|
639
|
+
if (entry.type === "implementation" && matchingOrganisations.some((org) => inside(source, org.project))) {
|
|
640
|
+
projectNotes.add(source);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const destination = [...notes][0] ?? [...projectNotes][0];
|
|
645
|
+
const sources = new Set;
|
|
646
|
+
const addChain = (source, boundary) => {
|
|
647
|
+
for (const ancestor of configFolders([source], boundary).reverse())
|
|
648
|
+
sources.add(ancestor);
|
|
649
|
+
sources.add(source);
|
|
650
|
+
};
|
|
651
|
+
const outerOrganisations = [...organisations].filter((source) => ![...organisations].some((parent) => parent !== source && inside(source, parent)));
|
|
652
|
+
for (const source of outerOrganisations)
|
|
653
|
+
addChain(source);
|
|
654
|
+
if (destination)
|
|
655
|
+
addChain(destination);
|
|
656
|
+
if (checkout)
|
|
657
|
+
sources.add(checkout);
|
|
658
|
+
if (folder !== checkout)
|
|
659
|
+
addChain(folder, checkout);
|
|
660
|
+
const requiredConfigs = new Set([
|
|
661
|
+
...configFolders([folder], checkout),
|
|
662
|
+
...sources
|
|
663
|
+
]);
|
|
664
|
+
for (const [owner, error] of configErrors) {
|
|
665
|
+
if (requiredConfigs.has(owner)) {
|
|
666
|
+
throw new Error(`Invalid Raggle config ${join2(owner, "raggle.json")}: ${error instanceof Error ? error.message : String(error)}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const blocks = [];
|
|
670
|
+
let remainingBytes = options.instructionBytes;
|
|
671
|
+
if (remainingBytes !== undefined) {
|
|
672
|
+
for (const source of sources) {
|
|
673
|
+
signal.throwIfAborted();
|
|
674
|
+
const instruction = readRepositoryInstructions(source, remainingBytes);
|
|
675
|
+
if (!instruction)
|
|
676
|
+
continue;
|
|
677
|
+
remainingBytes -= instruction.bytes;
|
|
678
|
+
if (instruction.content.trim())
|
|
679
|
+
blocks.push(instruction.content.trim());
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (destination && destination !== folder) {
|
|
683
|
+
blocks.push(`Write any planning or research content you create during this session (markdown, html files etc) to ${destination}`);
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
blocks,
|
|
687
|
+
linkedFolders: [...new Set([...notes, ...projectNotes])].filter((path) => path !== folder)
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// packages/llm/src/format.ts
|
|
692
|
+
function personalInstructionsBlock(personal) {
|
|
693
|
+
if (!personal.sources.length)
|
|
694
|
+
return;
|
|
695
|
+
return [
|
|
696
|
+
"## Personal agent instructions",
|
|
697
|
+
"",
|
|
698
|
+
`Supplemental instructions for ${personal.root}. Repository AGENTS.md instructions still apply. These files are included as text, not executed.`,
|
|
699
|
+
...personal.sources.flatMap((source) => [
|
|
700
|
+
"",
|
|
701
|
+
`### ${source.scope}: ${source.profile}`,
|
|
702
|
+
`Source: ${source.path}`,
|
|
703
|
+
"",
|
|
704
|
+
source.content
|
|
705
|
+
])
|
|
706
|
+
].join(`
|
|
707
|
+
`);
|
|
708
|
+
}
|
|
709
|
+
function pluginContextBlock(contributions) {
|
|
710
|
+
if (!contributions.length)
|
|
711
|
+
return;
|
|
712
|
+
return [
|
|
713
|
+
"## Plugin context",
|
|
714
|
+
...contributions.flatMap((plugin) => [
|
|
715
|
+
"",
|
|
716
|
+
`### ${plugin.label} (${plugin.id})`,
|
|
717
|
+
...plugin.sections.flatMap((section) => [
|
|
718
|
+
"",
|
|
719
|
+
`#### ${section.title}`,
|
|
720
|
+
...section.lines
|
|
721
|
+
])
|
|
722
|
+
])
|
|
723
|
+
].join(`
|
|
724
|
+
`);
|
|
725
|
+
}
|
|
726
|
+
function renderMessage(blocks) {
|
|
727
|
+
const text = blocks.map((block) => block?.trim()).filter(Boolean).join(`
|
|
728
|
+
|
|
729
|
+
`);
|
|
730
|
+
return text ? `${text}
|
|
731
|
+
` : "";
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// packages/llm/src/index.ts
|
|
735
|
+
var MAX_CONTEXT_BYTES = 1024 * 1024;
|
|
736
|
+
function realDirectory(folder) {
|
|
737
|
+
let path;
|
|
738
|
+
try {
|
|
739
|
+
path = realpathSync2(folder);
|
|
740
|
+
} catch {
|
|
741
|
+
throw new Error(`Folder does not exist: ${folder}`);
|
|
742
|
+
}
|
|
743
|
+
if (!statSync4(path).isDirectory())
|
|
744
|
+
throw new Error(`Not a folder: ${folder}`);
|
|
745
|
+
return path;
|
|
746
|
+
}
|
|
747
|
+
async function buildLlmContext(options) {
|
|
748
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
749
|
+
const includeInstructions = options.includeInstructions !== false;
|
|
750
|
+
const { depth, profile } = options;
|
|
751
|
+
if (depth !== undefined && depth !== "full" && !(Number.isSafeInteger(depth) && depth > 0)) {
|
|
752
|
+
throw new Error(`Depth must be a positive integer or "full": ${depth}`);
|
|
753
|
+
}
|
|
754
|
+
if (profile !== undefined) {
|
|
755
|
+
if (!includeInstructions) {
|
|
756
|
+
throw new Error("A profile cannot be used when instructions are omitted");
|
|
757
|
+
}
|
|
758
|
+
assertProfileName(profile);
|
|
759
|
+
}
|
|
760
|
+
signal.throwIfAborted();
|
|
761
|
+
const folder = realDirectory(options.folder);
|
|
762
|
+
const settings = includeInstructions ? readInstructionSettings() : undefined;
|
|
763
|
+
const agents = settings || depth !== undefined ? await buildAgentContext({
|
|
764
|
+
folder,
|
|
765
|
+
instructionBytes: settings?.maxBytes,
|
|
766
|
+
signal
|
|
767
|
+
}) : { blocks: [], linkedFolders: [] };
|
|
768
|
+
let blocks = agents.blocks;
|
|
769
|
+
let pluginBlock;
|
|
770
|
+
if (depth !== undefined) {
|
|
771
|
+
await Promise.resolve().then(() => init_context());
|
|
772
|
+
await Promise.resolve().then(() => init_plugins());
|
|
773
|
+
const core = await buildCoreContext({
|
|
774
|
+
folder,
|
|
775
|
+
depth,
|
|
776
|
+
signal
|
|
777
|
+
});
|
|
778
|
+
const contributions = await runContributors(options.plugins ?? [], {
|
|
779
|
+
folder,
|
|
780
|
+
linkedFolders: [
|
|
781
|
+
...new Set([...agents.linkedFolders, ...core.linkedFolders])
|
|
782
|
+
],
|
|
783
|
+
signal
|
|
784
|
+
});
|
|
785
|
+
blocks = [...agents.blocks, ...core.blocks];
|
|
786
|
+
pluginBlock = pluginContextBlock(contributions);
|
|
787
|
+
}
|
|
788
|
+
const personal = settings ? readPersonalInstructions(folder, settings, profile) : undefined;
|
|
789
|
+
const text = renderMessage([
|
|
790
|
+
...blocks,
|
|
791
|
+
personal && (depth === undefined ? personal.sources.map((source) => source.content).join(`
|
|
792
|
+
|
|
793
|
+
`) : personalInstructionsBlock(personal)),
|
|
794
|
+
pluginBlock
|
|
795
|
+
]);
|
|
796
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
797
|
+
if (bytes > MAX_CONTEXT_BYTES) {
|
|
798
|
+
throw new Error(`The context message is ${bytes} bytes, over the ${MAX_CONTEXT_BYTES}-byte limit. It is not truncated. Use the default agent-only mode, --no-instructions, or --no-plugins.`);
|
|
799
|
+
}
|
|
800
|
+
return text;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// packages/cli/src/llm/worker.ts
|
|
804
|
+
function send(message) {
|
|
805
|
+
return new Promise((resolve) => {
|
|
806
|
+
if (!process.send)
|
|
807
|
+
return resolve();
|
|
808
|
+
process.send(message, undefined, {}, () => resolve());
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
function installed(path) {
|
|
812
|
+
return import(new URL(path, import.meta.url).href);
|
|
813
|
+
}
|
|
814
|
+
async function contributors(folder) {
|
|
815
|
+
await send({ type: "stage", stage: "installed plugin loading" });
|
|
816
|
+
const external = await installed("../plugins/external.js");
|
|
817
|
+
external.usePluginBuild(await external.readPluginBuild());
|
|
818
|
+
const server = await installed("../site/server-plugins.js");
|
|
819
|
+
const registry = server.configuredFolderServerRegistry(await server.folderServerRegistryAsync(), await server.folderPluginRegistryConfig(folder));
|
|
820
|
+
return [...registry.values()].flatMap((plugin) => {
|
|
821
|
+
if (plugin.llm === undefined)
|
|
822
|
+
return [];
|
|
823
|
+
if (typeof plugin.llm !== "function")
|
|
824
|
+
throw new Error(`Plugin ${plugin.id} llm hook is not a function`);
|
|
825
|
+
const hook = plugin.llm.bind(plugin);
|
|
826
|
+
return [{
|
|
827
|
+
id: plugin.id,
|
|
828
|
+
label: plugin.label,
|
|
829
|
+
llm: (context) => {
|
|
830
|
+
send({ type: "stage", stage: `the ${plugin.id} plugin llm hook` });
|
|
831
|
+
return hook(context);
|
|
832
|
+
}
|
|
833
|
+
}];
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
function reportUnhandledFailure(error) {
|
|
837
|
+
send({
|
|
838
|
+
type: "result",
|
|
839
|
+
ok: false,
|
|
840
|
+
error: error instanceof Error ? error.message : String(error)
|
|
841
|
+
}).finally(() => process.exit(1));
|
|
842
|
+
}
|
|
843
|
+
process.once("uncaughtException", reportUnhandledFailure);
|
|
844
|
+
process.once("unhandledRejection", reportUnhandledFailure);
|
|
845
|
+
async function main() {
|
|
846
|
+
const request = JSON.parse(process.argv[2] ?? "");
|
|
847
|
+
const controller = new AbortController;
|
|
848
|
+
process.once("disconnect", () => controller.abort(new Error("Context generation was cancelled")));
|
|
849
|
+
try {
|
|
850
|
+
const plugins = request.includePlugins && request.depth !== undefined ? await contributors(request.folder) : [];
|
|
851
|
+
await send({ type: "stage", stage: "core context" });
|
|
852
|
+
const text = await buildLlmContext({
|
|
853
|
+
folder: request.folder,
|
|
854
|
+
depth: request.depth,
|
|
855
|
+
profile: request.profile,
|
|
856
|
+
includeInstructions: request.includeInstructions,
|
|
857
|
+
plugins,
|
|
858
|
+
signal: controller.signal
|
|
859
|
+
});
|
|
860
|
+
await send({ type: "result", ok: true, text });
|
|
861
|
+
} catch (error) {
|
|
862
|
+
await send({ type: "result", ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
863
|
+
}
|
|
864
|
+
process.exit(0);
|
|
865
|
+
}
|
|
866
|
+
await main();
|