agentsmith-cli 0.4.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/LICENSE +21 -0
- package/README.md +308 -0
- package/bin/agentsmith.js +26 -0
- package/dist/main.js +2930 -0
- package/dist/main.js.map +1 -0
- package/package.json +74 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,2930 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/utils/git.ts
|
|
22
|
+
var git_exports = {};
|
|
23
|
+
__export(git_exports, {
|
|
24
|
+
cloneRepo: () => cloneRepo,
|
|
25
|
+
getRepoName: () => getRepoName,
|
|
26
|
+
isGitHubUrl: () => isGitHubUrl,
|
|
27
|
+
normalizeGitHubUrl: () => normalizeGitHubUrl,
|
|
28
|
+
resolveInput: () => resolveInput
|
|
29
|
+
});
|
|
30
|
+
import { simpleGit } from "simple-git";
|
|
31
|
+
import fs6 from "fs/promises";
|
|
32
|
+
import path7 from "path";
|
|
33
|
+
import os from "os";
|
|
34
|
+
import crypto from "crypto";
|
|
35
|
+
function isGitHubUrl(input) {
|
|
36
|
+
return input.startsWith("https://github.com/") || input.startsWith("git@github.com:") || input.startsWith("github.com/");
|
|
37
|
+
}
|
|
38
|
+
function normalizeGitHubUrl(input) {
|
|
39
|
+
let url = input;
|
|
40
|
+
if (url.startsWith("github.com/")) {
|
|
41
|
+
url = `https://${url}`;
|
|
42
|
+
} else if (url.startsWith("git@github.com:")) {
|
|
43
|
+
url = url.replace("git@github.com:", "https://github.com/");
|
|
44
|
+
}
|
|
45
|
+
return url.replace(/\.git$/, "");
|
|
46
|
+
}
|
|
47
|
+
function getRepoName(url) {
|
|
48
|
+
const normalized = normalizeGitHubUrl(url);
|
|
49
|
+
const parts = normalized.split("/");
|
|
50
|
+
return parts[parts.length - 1] || "repo";
|
|
51
|
+
}
|
|
52
|
+
async function cloneRepo(url) {
|
|
53
|
+
const normalizedUrl = normalizeGitHubUrl(url);
|
|
54
|
+
const repoName = getRepoName(url);
|
|
55
|
+
const hash = crypto.randomBytes(4).toString("hex");
|
|
56
|
+
const tempDir = path7.join(os.tmpdir(), `agentsmith-${repoName}-${hash}`);
|
|
57
|
+
await fs6.mkdir(tempDir, { recursive: true });
|
|
58
|
+
const git = simpleGit();
|
|
59
|
+
await git.clone(normalizedUrl, tempDir, ["--depth", "1"]);
|
|
60
|
+
return {
|
|
61
|
+
path: tempDir,
|
|
62
|
+
isTemporary: true,
|
|
63
|
+
cleanup: async () => {
|
|
64
|
+
try {
|
|
65
|
+
await fs6.rm(tempDir, { recursive: true, force: true });
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async function resolveInput(input) {
|
|
72
|
+
if (isGitHubUrl(input)) {
|
|
73
|
+
return cloneRepo(input);
|
|
74
|
+
}
|
|
75
|
+
const absolutePath = path7.resolve(input);
|
|
76
|
+
return {
|
|
77
|
+
path: absolutePath,
|
|
78
|
+
isTemporary: false,
|
|
79
|
+
cleanup: async () => {
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
var init_git = __esm({
|
|
84
|
+
"src/utils/git.ts"() {
|
|
85
|
+
"use strict";
|
|
86
|
+
init_esm_shims();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// src/utils/license.ts
|
|
91
|
+
var license_exports = {};
|
|
92
|
+
__export(license_exports, {
|
|
93
|
+
detectLicense: () => detectLicense,
|
|
94
|
+
formatLicenseStatus: () => formatLicenseStatus,
|
|
95
|
+
isPermissiveLicense: () => isPermissiveLicense
|
|
96
|
+
});
|
|
97
|
+
import fs7 from "fs/promises";
|
|
98
|
+
import path8 from "path";
|
|
99
|
+
async function detectLicense(repoPath) {
|
|
100
|
+
for (const filename of LICENSE_FILES) {
|
|
101
|
+
const licensePath = path8.join(repoPath, filename);
|
|
102
|
+
try {
|
|
103
|
+
const content = await fs7.readFile(licensePath, "utf-8");
|
|
104
|
+
const result = identifyLicense(content);
|
|
105
|
+
if (result.detected) {
|
|
106
|
+
return {
|
|
107
|
+
...result,
|
|
108
|
+
file: filename
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
detected: true,
|
|
113
|
+
name: "Unknown",
|
|
114
|
+
spdxId: null,
|
|
115
|
+
permissive: false,
|
|
116
|
+
file: filename
|
|
117
|
+
};
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const packageJsonPath = path8.join(repoPath, "package.json");
|
|
123
|
+
const content = await fs7.readFile(packageJsonPath, "utf-8");
|
|
124
|
+
const pkg = JSON.parse(content);
|
|
125
|
+
if (pkg.license) {
|
|
126
|
+
const spdxId = pkg.license;
|
|
127
|
+
const isPermissive = Object.keys(PERMISSIVE_LICENSES).some(
|
|
128
|
+
(key) => key.toLowerCase() === spdxId.toLowerCase()
|
|
129
|
+
);
|
|
130
|
+
return {
|
|
131
|
+
detected: true,
|
|
132
|
+
name: spdxId,
|
|
133
|
+
spdxId,
|
|
134
|
+
permissive: isPermissive,
|
|
135
|
+
file: "package.json"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const pyprojectPath = path8.join(repoPath, "pyproject.toml");
|
|
142
|
+
const content = await fs7.readFile(pyprojectPath, "utf-8");
|
|
143
|
+
const licenseMatch = content.match(/license\s*=\s*["{]([^"}]+)["}]/i);
|
|
144
|
+
if (licenseMatch) {
|
|
145
|
+
const licenseName = licenseMatch[1].trim();
|
|
146
|
+
const isPermissive = Object.keys(PERMISSIVE_LICENSES).some(
|
|
147
|
+
(key) => key.toLowerCase() === licenseName.toLowerCase()
|
|
148
|
+
);
|
|
149
|
+
return {
|
|
150
|
+
detected: true,
|
|
151
|
+
name: licenseName,
|
|
152
|
+
spdxId: licenseName,
|
|
153
|
+
permissive: isPermissive,
|
|
154
|
+
file: "pyproject.toml"
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
detected: false,
|
|
161
|
+
name: null,
|
|
162
|
+
spdxId: null,
|
|
163
|
+
permissive: false,
|
|
164
|
+
file: null
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function identifyLicense(content) {
|
|
168
|
+
const lowerContent = content.toLowerCase();
|
|
169
|
+
for (const [spdxId, patterns] of Object.entries(PERMISSIVE_LICENSES)) {
|
|
170
|
+
for (const pattern of patterns) {
|
|
171
|
+
if (lowerContent.includes(pattern)) {
|
|
172
|
+
return {
|
|
173
|
+
detected: true,
|
|
174
|
+
name: spdxId,
|
|
175
|
+
spdxId,
|
|
176
|
+
permissive: true
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const proprietaryPatterns = [
|
|
182
|
+
"all rights reserved",
|
|
183
|
+
"proprietary",
|
|
184
|
+
"confidential",
|
|
185
|
+
"not for redistribution",
|
|
186
|
+
"may not be copied"
|
|
187
|
+
];
|
|
188
|
+
for (const pattern of proprietaryPatterns) {
|
|
189
|
+
if (lowerContent.includes(pattern) && !lowerContent.includes("mit")) {
|
|
190
|
+
return {
|
|
191
|
+
detected: true,
|
|
192
|
+
name: "Proprietary",
|
|
193
|
+
spdxId: null,
|
|
194
|
+
permissive: false
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
detected: false,
|
|
200
|
+
name: null,
|
|
201
|
+
spdxId: null,
|
|
202
|
+
permissive: false
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function isPermissiveLicense(spdxId) {
|
|
206
|
+
if (!spdxId) return false;
|
|
207
|
+
return Object.keys(PERMISSIVE_LICENSES).some(
|
|
208
|
+
(key) => key.toLowerCase() === spdxId.toLowerCase()
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
function formatLicenseStatus(license) {
|
|
212
|
+
if (!license.detected) {
|
|
213
|
+
return "No license detected";
|
|
214
|
+
}
|
|
215
|
+
if (license.permissive) {
|
|
216
|
+
return `${license.name} (permissive)`;
|
|
217
|
+
}
|
|
218
|
+
return `${license.name} (not permissive)`;
|
|
219
|
+
}
|
|
220
|
+
var PERMISSIVE_LICENSES, LICENSE_FILES;
|
|
221
|
+
var init_license = __esm({
|
|
222
|
+
"src/utils/license.ts"() {
|
|
223
|
+
"use strict";
|
|
224
|
+
init_esm_shims();
|
|
225
|
+
PERMISSIVE_LICENSES = {
|
|
226
|
+
// MIT family
|
|
227
|
+
"MIT": ["mit license", "the mit license", "mit-license"],
|
|
228
|
+
// Apache
|
|
229
|
+
"Apache-2.0": ["apache license", "apache-2.0", "apache 2.0", "licensed under the apache license"],
|
|
230
|
+
// BSD family
|
|
231
|
+
"BSD-2-Clause": ["bsd 2-clause", "bsd-2-clause", "simplified bsd", "freebsd license"],
|
|
232
|
+
"BSD-3-Clause": ["bsd 3-clause", "bsd-3-clause", "new bsd", "modified bsd"],
|
|
233
|
+
"0BSD": ["zero-clause bsd", "0bsd"],
|
|
234
|
+
// GPL family (copyleft but permissive for our purposes)
|
|
235
|
+
"GPL-2.0": ["gnu general public license v2", "gpl-2.0", "gplv2", "gnu gpl v2"],
|
|
236
|
+
"GPL-3.0": ["gnu general public license v3", "gpl-3.0", "gplv3", "gnu gpl v3"],
|
|
237
|
+
"LGPL-2.1": ["gnu lesser general public license v2.1", "lgpl-2.1", "lgplv2.1"],
|
|
238
|
+
"LGPL-3.0": ["gnu lesser general public license v3", "lgpl-3.0", "lgplv3"],
|
|
239
|
+
"AGPL-3.0": ["gnu affero general public license", "agpl-3.0", "agplv3"],
|
|
240
|
+
// Other permissive
|
|
241
|
+
"ISC": ["isc license"],
|
|
242
|
+
"MPL-2.0": ["mozilla public license", "mpl-2.0", "mpl 2.0"],
|
|
243
|
+
"Unlicense": ["unlicense", "this is free and unencumbered software"],
|
|
244
|
+
"CC0-1.0": ["cc0", "creative commons zero", "cc0-1.0"],
|
|
245
|
+
"WTFPL": ["wtfpl", "do what the fuck you want"],
|
|
246
|
+
"Zlib": ["zlib license"],
|
|
247
|
+
"BlueOak-1.0.0": ["blue oak model license"]
|
|
248
|
+
};
|
|
249
|
+
LICENSE_FILES = [
|
|
250
|
+
"LICENSE",
|
|
251
|
+
"LICENSE.md",
|
|
252
|
+
"LICENSE.txt",
|
|
253
|
+
"LICENCE",
|
|
254
|
+
"LICENCE.md",
|
|
255
|
+
"LICENCE.txt",
|
|
256
|
+
"license",
|
|
257
|
+
"license.md",
|
|
258
|
+
"license.txt",
|
|
259
|
+
"COPYING",
|
|
260
|
+
"COPYING.md",
|
|
261
|
+
"COPYING.txt"
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// src/main.ts
|
|
267
|
+
init_esm_shims();
|
|
268
|
+
import { Command } from "commander";
|
|
269
|
+
import chalk5 from "chalk";
|
|
270
|
+
|
|
271
|
+
// src/commands/assimilate.ts
|
|
272
|
+
init_esm_shims();
|
|
273
|
+
import chalk2 from "chalk";
|
|
274
|
+
|
|
275
|
+
// src/scanner/index.ts
|
|
276
|
+
init_esm_shims();
|
|
277
|
+
import fs from "fs/promises";
|
|
278
|
+
import path2 from "path";
|
|
279
|
+
import { glob } from "glob";
|
|
280
|
+
var IGNORE_PATTERNS = [
|
|
281
|
+
"**/node_modules/**",
|
|
282
|
+
"**/.git/**",
|
|
283
|
+
"**/dist/**",
|
|
284
|
+
"**/build/**",
|
|
285
|
+
"**/.next/**",
|
|
286
|
+
"**/coverage/**",
|
|
287
|
+
"**/__pycache__/**",
|
|
288
|
+
"**/.venv/**",
|
|
289
|
+
"**/venv/**",
|
|
290
|
+
"**/.env",
|
|
291
|
+
"**/*.lock",
|
|
292
|
+
"**/package-lock.json",
|
|
293
|
+
"**/yarn.lock",
|
|
294
|
+
"**/pnpm-lock.yaml"
|
|
295
|
+
];
|
|
296
|
+
var CONFIG_PATTERNS = [
|
|
297
|
+
"package.json",
|
|
298
|
+
"tsconfig.json",
|
|
299
|
+
"pyproject.toml",
|
|
300
|
+
"setup.py",
|
|
301
|
+
"go.mod",
|
|
302
|
+
"Cargo.toml",
|
|
303
|
+
".eslintrc*",
|
|
304
|
+
".prettierrc*",
|
|
305
|
+
"docker-compose*.yml",
|
|
306
|
+
"Dockerfile",
|
|
307
|
+
".github/workflows/*.yml"
|
|
308
|
+
];
|
|
309
|
+
var Scanner = class {
|
|
310
|
+
rootPath;
|
|
311
|
+
verbose;
|
|
312
|
+
constructor(rootPath, verbose = false) {
|
|
313
|
+
this.rootPath = rootPath;
|
|
314
|
+
this.verbose = verbose;
|
|
315
|
+
}
|
|
316
|
+
async scan() {
|
|
317
|
+
const allFiles = await glob("**/*", {
|
|
318
|
+
cwd: this.rootPath,
|
|
319
|
+
nodir: true,
|
|
320
|
+
ignore: IGNORE_PATTERNS,
|
|
321
|
+
absolute: false
|
|
322
|
+
});
|
|
323
|
+
const files = [];
|
|
324
|
+
for (const relativePath of allFiles) {
|
|
325
|
+
const fullPath = path2.join(this.rootPath, relativePath);
|
|
326
|
+
try {
|
|
327
|
+
const stat = await fs.stat(fullPath);
|
|
328
|
+
files.push({
|
|
329
|
+
path: fullPath,
|
|
330
|
+
relativePath,
|
|
331
|
+
extension: path2.extname(relativePath),
|
|
332
|
+
size: stat.size,
|
|
333
|
+
isTest: this.isTestFile(relativePath),
|
|
334
|
+
isConfig: this.isConfigFile(relativePath)
|
|
335
|
+
});
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const language = this.detectLanguage(files);
|
|
340
|
+
const framework = await this.detectFramework(files);
|
|
341
|
+
const configFiles = files.filter((f) => f.isConfig).map((f) => f.relativePath);
|
|
342
|
+
const testFiles = files.filter((f) => f.isTest).map((f) => f.relativePath);
|
|
343
|
+
const sourceDirectories = this.detectSourceDirectories(files);
|
|
344
|
+
return {
|
|
345
|
+
rootPath: this.rootPath,
|
|
346
|
+
files,
|
|
347
|
+
language,
|
|
348
|
+
framework,
|
|
349
|
+
configFiles,
|
|
350
|
+
testFiles,
|
|
351
|
+
sourceDirectories
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
detectLanguage(files) {
|
|
355
|
+
const extCounts = {};
|
|
356
|
+
for (const file of files) {
|
|
357
|
+
if (file.extension) {
|
|
358
|
+
extCounts[file.extension] = (extCounts[file.extension] || 0) + 1;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const languageMap = {
|
|
362
|
+
".ts": "TypeScript",
|
|
363
|
+
".tsx": "TypeScript",
|
|
364
|
+
".js": "JavaScript",
|
|
365
|
+
".jsx": "JavaScript",
|
|
366
|
+
".py": "Python",
|
|
367
|
+
".go": "Go",
|
|
368
|
+
".rs": "Rust",
|
|
369
|
+
".java": "Java",
|
|
370
|
+
".cs": "C#",
|
|
371
|
+
".rb": "Ruby",
|
|
372
|
+
".php": "PHP"
|
|
373
|
+
};
|
|
374
|
+
let maxCount = 0;
|
|
375
|
+
let detectedLang = "Unknown";
|
|
376
|
+
for (const [ext, lang] of Object.entries(languageMap)) {
|
|
377
|
+
if (extCounts[ext] && extCounts[ext] > maxCount) {
|
|
378
|
+
maxCount = extCounts[ext];
|
|
379
|
+
detectedLang = lang;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (detectedLang === "JavaScript" && files.some((f) => f.relativePath.includes("tsconfig"))) {
|
|
383
|
+
detectedLang = "TypeScript";
|
|
384
|
+
}
|
|
385
|
+
return detectedLang;
|
|
386
|
+
}
|
|
387
|
+
async detectFramework(files) {
|
|
388
|
+
const fileSet = new Set(files.map((f) => f.relativePath));
|
|
389
|
+
const hasFile = (name) => fileSet.has(name);
|
|
390
|
+
if (hasFile("package.json")) {
|
|
391
|
+
try {
|
|
392
|
+
const content = await fs.readFile(path2.join(this.rootPath, "package.json"), "utf-8");
|
|
393
|
+
const pkg = JSON.parse(content);
|
|
394
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
395
|
+
if (deps["next"]) return "Next.js";
|
|
396
|
+
if (deps["react"]) return "React";
|
|
397
|
+
if (deps["vue"]) return "Vue";
|
|
398
|
+
if (deps["@angular/core"]) return "Angular";
|
|
399
|
+
if (deps["express"]) return "Express.js";
|
|
400
|
+
if (deps["fastify"]) return "Fastify";
|
|
401
|
+
if (deps["nestjs"]) return "NestJS";
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (hasFile("pyproject.toml") || hasFile("requirements.txt")) {
|
|
406
|
+
const reqPath = hasFile("requirements.txt") ? path2.join(this.rootPath, "requirements.txt") : null;
|
|
407
|
+
if (reqPath) {
|
|
408
|
+
try {
|
|
409
|
+
const content = await fs.readFile(reqPath, "utf-8");
|
|
410
|
+
if (content.includes("django")) return "Django";
|
|
411
|
+
if (content.includes("flask")) return "Flask";
|
|
412
|
+
if (content.includes("fastapi")) return "FastAPI";
|
|
413
|
+
} catch {
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (hasFile("go.mod")) {
|
|
418
|
+
try {
|
|
419
|
+
const content = await fs.readFile(path2.join(this.rootPath, "go.mod"), "utf-8");
|
|
420
|
+
if (content.includes("gin-gonic")) return "Gin";
|
|
421
|
+
if (content.includes("echo")) return "Echo";
|
|
422
|
+
if (content.includes("fiber")) return "Fiber";
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
isTestFile(relativePath) {
|
|
429
|
+
const lower = relativePath.toLowerCase();
|
|
430
|
+
const normalized = lower.replace(/\\/g, "/");
|
|
431
|
+
return normalized.includes(".test.") || normalized.includes(".spec.") || normalized.includes("_test.") || normalized.includes("test_") || normalized.startsWith("tests/") || normalized.startsWith("test/") || normalized.startsWith("__tests__/");
|
|
432
|
+
}
|
|
433
|
+
isConfigFile(relativePath) {
|
|
434
|
+
const basename = path2.basename(relativePath);
|
|
435
|
+
return CONFIG_PATTERNS.some((pattern) => {
|
|
436
|
+
if (pattern.includes("*")) {
|
|
437
|
+
const regex = new RegExp(pattern.replace("*", ".*"));
|
|
438
|
+
return regex.test(basename);
|
|
439
|
+
}
|
|
440
|
+
return basename === pattern || relativePath.includes(pattern.replace("*", ""));
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
detectSourceDirectories(files) {
|
|
444
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
445
|
+
const commonSrcDirs = ["src", "lib", "app", "pkg", "internal", "cmd"];
|
|
446
|
+
for (const file of files) {
|
|
447
|
+
if (file.isTest || file.isConfig) continue;
|
|
448
|
+
const parts = file.relativePath.split(path2.sep);
|
|
449
|
+
if (parts.length > 1) {
|
|
450
|
+
const firstDir = parts[0];
|
|
451
|
+
if (commonSrcDirs.includes(firstDir)) {
|
|
452
|
+
dirs.add(firstDir);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (dirs.size === 0) {
|
|
457
|
+
const dirCounts = {};
|
|
458
|
+
for (const file of files) {
|
|
459
|
+
const parts = file.relativePath.split(path2.sep);
|
|
460
|
+
if (parts.length > 1 && !file.isTest && !file.isConfig) {
|
|
461
|
+
dirCounts[parts[0]] = (dirCounts[parts[0]] || 0) + 1;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const sorted = Object.entries(dirCounts).sort((a, b) => b[1] - a[1]);
|
|
465
|
+
for (const [dir] of sorted.slice(0, 3)) {
|
|
466
|
+
dirs.add(dir);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return Array.from(dirs);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// src/analyzer/index.ts
|
|
474
|
+
init_esm_shims();
|
|
475
|
+
|
|
476
|
+
// src/analyzer/core.ts
|
|
477
|
+
init_esm_shims();
|
|
478
|
+
|
|
479
|
+
// src/analyzer/schemas.ts
|
|
480
|
+
init_esm_shims();
|
|
481
|
+
import { z } from "zod";
|
|
482
|
+
var SkillOutputSchema = z.object({
|
|
483
|
+
name: z.string().min(1).max(64),
|
|
484
|
+
description: z.string().min(1),
|
|
485
|
+
sourceDir: z.string().default(""),
|
|
486
|
+
patterns: z.array(z.string()).default([]),
|
|
487
|
+
triggers: z.array(z.string()).default([]),
|
|
488
|
+
category: z.enum(["architecture", "reliability", "quality", "security", "patterns"]).default("patterns"),
|
|
489
|
+
examples: z.array(z.string()).default([])
|
|
490
|
+
});
|
|
491
|
+
var ToolOutputSchema = z.union([
|
|
492
|
+
z.object({
|
|
493
|
+
name: z.string().min(1),
|
|
494
|
+
command: z.string().default(""),
|
|
495
|
+
description: z.string().default("")
|
|
496
|
+
}),
|
|
497
|
+
z.string().min(1)
|
|
498
|
+
]);
|
|
499
|
+
var HookOutputSchema = z.object({
|
|
500
|
+
name: z.string().min(1),
|
|
501
|
+
event: z.string().min(1),
|
|
502
|
+
description: z.string().default(""),
|
|
503
|
+
commands: z.array(z.string()).default([]),
|
|
504
|
+
condition: z.string().optional()
|
|
505
|
+
});
|
|
506
|
+
var AgentOutputSchema = z.lazy(
|
|
507
|
+
() => z.object({
|
|
508
|
+
name: z.string().min(1),
|
|
509
|
+
description: z.string().default(""),
|
|
510
|
+
skills: z.array(z.string()).default([]),
|
|
511
|
+
tools: z.array(ToolOutputSchema).default([]),
|
|
512
|
+
isSubAgent: z.boolean().default(false),
|
|
513
|
+
parentAgent: z.string().optional(),
|
|
514
|
+
subAgents: z.array(z.union([AgentOutputSchema, z.string()])).default([]),
|
|
515
|
+
triggers: z.array(z.string()).default([]),
|
|
516
|
+
sourceDir: z.string().optional()
|
|
517
|
+
})
|
|
518
|
+
);
|
|
519
|
+
var AnalysisOutputSchema = z.object({
|
|
520
|
+
skills: z.array(SkillOutputSchema).default([]),
|
|
521
|
+
agents: z.array(AgentOutputSchema).default([]),
|
|
522
|
+
summary: z.string().default(""),
|
|
523
|
+
hooks: z.array(HookOutputSchema).optional()
|
|
524
|
+
});
|
|
525
|
+
function validateAnalysisOutput(data) {
|
|
526
|
+
const result = AnalysisOutputSchema.safeParse(data);
|
|
527
|
+
if (result.success) {
|
|
528
|
+
return result.data;
|
|
529
|
+
}
|
|
530
|
+
for (const issue of result.error.issues) {
|
|
531
|
+
const path10 = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
532
|
+
console.warn(` [Zod] ${path10}: ${issue.message}`);
|
|
533
|
+
}
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/analyzer/core.ts
|
|
538
|
+
function flattenAgents(agents) {
|
|
539
|
+
const result = [];
|
|
540
|
+
for (const rawAgent of agents) {
|
|
541
|
+
if (!rawAgent || typeof rawAgent !== "object") continue;
|
|
542
|
+
const agent = rawAgent;
|
|
543
|
+
const nestedSubAgents = [];
|
|
544
|
+
const subAgentNames = [];
|
|
545
|
+
const subAgentsArray = agent.subAgents;
|
|
546
|
+
if (Array.isArray(subAgentsArray) && subAgentsArray.length > 0) {
|
|
547
|
+
for (const subAgent of subAgentsArray) {
|
|
548
|
+
if (typeof subAgent === "object" && subAgent !== null && "name" in subAgent) {
|
|
549
|
+
const nestedAgent = subAgent;
|
|
550
|
+
nestedAgent.isSubAgent = true;
|
|
551
|
+
nestedAgent.parentAgent = agent.name;
|
|
552
|
+
nestedSubAgents.push(nestedAgent);
|
|
553
|
+
subAgentNames.push(nestedAgent.name);
|
|
554
|
+
} else if (typeof subAgent === "string") {
|
|
555
|
+
subAgentNames.push(subAgent);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const normalizedAgent = {
|
|
560
|
+
name: String(agent.name || "unknown"),
|
|
561
|
+
description: String(agent.description || ""),
|
|
562
|
+
skills: Array.isArray(agent.skills) ? agent.skills.map((s) => String(s)) : [],
|
|
563
|
+
tools: normalizeTools(agent.tools),
|
|
564
|
+
isSubAgent: Boolean(agent.isSubAgent),
|
|
565
|
+
parentAgent: agent.parentAgent ? String(agent.parentAgent) : void 0,
|
|
566
|
+
subAgents: subAgentNames.length > 0 ? subAgentNames : void 0,
|
|
567
|
+
triggers: Array.isArray(agent.triggers) ? agent.triggers.map((t) => String(t)) : [],
|
|
568
|
+
sourceDir: agent.sourceDir ? String(agent.sourceDir) : void 0
|
|
569
|
+
};
|
|
570
|
+
result.push(normalizedAgent);
|
|
571
|
+
if (nestedSubAgents.length > 0) {
|
|
572
|
+
result.push(...flattenAgents(nestedSubAgents));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
577
|
+
function normalizeTools(tools) {
|
|
578
|
+
if (!tools || !Array.isArray(tools)) {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
return tools.map((tool) => {
|
|
582
|
+
if (typeof tool === "string") {
|
|
583
|
+
return {
|
|
584
|
+
name: tool.split(" ")[0],
|
|
585
|
+
command: tool,
|
|
586
|
+
description: tool
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
if (typeof tool === "object" && tool !== null) {
|
|
590
|
+
const t = tool;
|
|
591
|
+
return {
|
|
592
|
+
name: String(t.name || "unknown"),
|
|
593
|
+
command: String(t.command || t.name || ""),
|
|
594
|
+
description: String(t.description || t.name || "")
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
name: "unknown",
|
|
599
|
+
command: String(tool),
|
|
600
|
+
description: String(tool)
|
|
601
|
+
};
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function extractAllTools(agents) {
|
|
605
|
+
const tools = [];
|
|
606
|
+
for (const agent of agents) {
|
|
607
|
+
if (agent.tools) {
|
|
608
|
+
tools.push(...agent.tools);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return tools;
|
|
612
|
+
}
|
|
613
|
+
function detectDomainBoundaries(files, pathSep) {
|
|
614
|
+
const domains = /* @__PURE__ */ new Map();
|
|
615
|
+
const domainPatterns = [
|
|
616
|
+
"api",
|
|
617
|
+
"auth",
|
|
618
|
+
"backend",
|
|
619
|
+
"frontend",
|
|
620
|
+
"core",
|
|
621
|
+
"common",
|
|
622
|
+
"shared",
|
|
623
|
+
"services",
|
|
624
|
+
"handlers",
|
|
625
|
+
"controllers",
|
|
626
|
+
"models",
|
|
627
|
+
"views",
|
|
628
|
+
"routes",
|
|
629
|
+
"components",
|
|
630
|
+
"hooks",
|
|
631
|
+
"utils",
|
|
632
|
+
"lib",
|
|
633
|
+
"pkg",
|
|
634
|
+
"internal",
|
|
635
|
+
"cmd",
|
|
636
|
+
"client",
|
|
637
|
+
"server",
|
|
638
|
+
"admin",
|
|
639
|
+
"public",
|
|
640
|
+
"private",
|
|
641
|
+
"modules"
|
|
642
|
+
];
|
|
643
|
+
for (const file of files) {
|
|
644
|
+
if (file.isTest || file.isConfig) continue;
|
|
645
|
+
const parts = file.relativePath.split(pathSep);
|
|
646
|
+
if (parts.length < 2) continue;
|
|
647
|
+
const topDir = parts[0];
|
|
648
|
+
const secondDir = parts.length > 2 ? parts[1] : null;
|
|
649
|
+
if (!domains.has(topDir)) {
|
|
650
|
+
domains.set(topDir, { path: topDir, fileCount: 0, subDirs: /* @__PURE__ */ new Set() });
|
|
651
|
+
}
|
|
652
|
+
const domain = domains.get(topDir);
|
|
653
|
+
domain.fileCount++;
|
|
654
|
+
if (secondDir && domainPatterns.includes(secondDir.toLowerCase())) {
|
|
655
|
+
domain.subDirs.add(secondDir);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return Array.from(domains.entries()).filter(([name, info]) => info.fileCount > 5 && domainPatterns.includes(name.toLowerCase())).map(([name, info]) => ({ name, path: info.path, fileCount: info.fileCount })).sort((a, b) => b.fileCount - a.fileCount);
|
|
659
|
+
}
|
|
660
|
+
function generateDefaultHooks(language, testFilesExist) {
|
|
661
|
+
const hooks = [];
|
|
662
|
+
if (language === "TypeScript" || language === "JavaScript") {
|
|
663
|
+
hooks.push({
|
|
664
|
+
name: "pre-commit-quality",
|
|
665
|
+
event: "pre-commit",
|
|
666
|
+
description: "Run linting and type checking before commit",
|
|
667
|
+
commands: ["npm run lint", "npm run build"]
|
|
668
|
+
});
|
|
669
|
+
if (testFilesExist) {
|
|
670
|
+
hooks.push({
|
|
671
|
+
name: "pre-push-tests",
|
|
672
|
+
event: "pre-push",
|
|
673
|
+
description: "Run tests before pushing",
|
|
674
|
+
commands: ["npm test"]
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
} else if (language === "Python") {
|
|
678
|
+
hooks.push({
|
|
679
|
+
name: "pre-commit-quality",
|
|
680
|
+
event: "pre-commit",
|
|
681
|
+
description: "Run linting and formatting before commit",
|
|
682
|
+
commands: ["ruff check .", "ruff format --check ."]
|
|
683
|
+
});
|
|
684
|
+
if (testFilesExist) {
|
|
685
|
+
hooks.push({
|
|
686
|
+
name: "pre-push-tests",
|
|
687
|
+
event: "pre-push",
|
|
688
|
+
description: "Run tests before pushing",
|
|
689
|
+
commands: ["pytest"]
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
} else if (language === "Go") {
|
|
693
|
+
hooks.push({
|
|
694
|
+
name: "pre-commit-quality",
|
|
695
|
+
event: "pre-commit",
|
|
696
|
+
description: "Run linting and formatting before commit",
|
|
697
|
+
commands: ["go fmt ./...", "golangci-lint run"]
|
|
698
|
+
});
|
|
699
|
+
if (testFilesExist) {
|
|
700
|
+
hooks.push({
|
|
701
|
+
name: "pre-push-tests",
|
|
702
|
+
event: "pre-push",
|
|
703
|
+
description: "Run tests before pushing",
|
|
704
|
+
commands: ["go test ./..."]
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
hooks.push({
|
|
709
|
+
name: "post-generate-validate",
|
|
710
|
+
event: "post-generate",
|
|
711
|
+
description: "Validate generated agent assets after generation",
|
|
712
|
+
commands: ["npx agentsmith validate"]
|
|
713
|
+
});
|
|
714
|
+
return hooks;
|
|
715
|
+
}
|
|
716
|
+
function detectToolsFromConfig(language, configFiles) {
|
|
717
|
+
const tools = [];
|
|
718
|
+
if (language === "TypeScript" || language === "JavaScript") {
|
|
719
|
+
if (configFiles.includes("package.json")) {
|
|
720
|
+
tools.push(
|
|
721
|
+
{ name: "install", command: "npm install", description: "Install dependencies" },
|
|
722
|
+
{ name: "build", command: "npm run build", description: "Build the project" },
|
|
723
|
+
{ name: "test", command: "npm test", description: "Run tests" },
|
|
724
|
+
{ name: "lint", command: "npm run lint", description: "Run linter" }
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
} else if (language === "Python") {
|
|
728
|
+
tools.push(
|
|
729
|
+
{ name: "install", command: "pip install -e .", description: "Install dependencies" },
|
|
730
|
+
{ name: "test", command: "pytest", description: "Run tests" },
|
|
731
|
+
{ name: "lint", command: "ruff check .", description: "Run linter" }
|
|
732
|
+
);
|
|
733
|
+
} else if (language === "Go") {
|
|
734
|
+
tools.push(
|
|
735
|
+
{ name: "build", command: "go build ./...", description: "Build the project" },
|
|
736
|
+
{ name: "test", command: "go test ./...", description: "Run tests" },
|
|
737
|
+
{ name: "lint", command: "golangci-lint run", description: "Run linter" }
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
return tools;
|
|
741
|
+
}
|
|
742
|
+
function getSystemPrompt(language, domains) {
|
|
743
|
+
const domainHints = domains && domains.length > 0 ? `
|
|
744
|
+
Potential domains detected: ${domains.map((d) => d.name).join(", ")}` : "";
|
|
745
|
+
return `You are Agent Smith, an AI designed to assimilate repositories into agent hierarchies.
|
|
746
|
+
|
|
747
|
+
Your task: Analyze this ${language} repository and extract:
|
|
748
|
+
1. SKILLS - Reusable patterns, conventions, and capabilities specific to parts of this codebase
|
|
749
|
+
2. AGENTS - Domain-specific agents with clear responsibilities
|
|
750
|
+
3. SUB-AGENTS - Child agents for complex domains (e.g., auth/oauth, auth/rbac under auth)
|
|
751
|
+
4. NESTED AGENTS - Multi-level hierarchies for large codebases
|
|
752
|
+
5. TOOLS - Commands that can be run (build, test, lint, deploy, etc.)
|
|
753
|
+
|
|
754
|
+
## Agent Hierarchy Guidelines
|
|
755
|
+
- Create a ROOT agent for the overall repository
|
|
756
|
+
- Create SUB-AGENTS for major domains (backend, frontend, api, auth, data)
|
|
757
|
+
- Create NESTED agents when a domain has sub-domains (api/v1, api/v2, api/graphql)
|
|
758
|
+
- Each agent should have 2-5 skills max; split if more are needed
|
|
759
|
+
${domainHints}
|
|
760
|
+
|
|
761
|
+
For each SKILL, identify:
|
|
762
|
+
- A kebab-case name (max 64 chars)
|
|
763
|
+
- A clear description of when to use it
|
|
764
|
+
- The source directory it relates to
|
|
765
|
+
- The patterns and conventions it embodies
|
|
766
|
+
- Keywords that trigger its relevance
|
|
767
|
+
- Category: architecture, reliability, quality, security, or patterns
|
|
768
|
+
|
|
769
|
+
For AGENTS, determine:
|
|
770
|
+
- Natural domain boundaries based on directory structure
|
|
771
|
+
- Parent-child relationships (isSubAgent: true for children)
|
|
772
|
+
- Which skills each agent owns (skills should not overlap between agents)
|
|
773
|
+
- Tools specific to that agent's domain
|
|
774
|
+
|
|
775
|
+
Respond in valid JSON format only. No markdown, no explanation.`;
|
|
776
|
+
}
|
|
777
|
+
function buildAnalysisPrompt(language, framework, sourceDirectories, configFiles, fileList, sampleContent) {
|
|
778
|
+
return `Analyze this ${language} repository.
|
|
779
|
+
|
|
780
|
+
## Repository Structure
|
|
781
|
+
Language: ${language}
|
|
782
|
+
Framework: ${framework || "None"}
|
|
783
|
+
Source directories: ${sourceDirectories.join(", ")}
|
|
784
|
+
Config files: ${configFiles.join(", ")}
|
|
785
|
+
|
|
786
|
+
## File List (first 100)
|
|
787
|
+
${fileList}
|
|
788
|
+
|
|
789
|
+
## File Samples
|
|
790
|
+
${sampleContent}
|
|
791
|
+
|
|
792
|
+
## Instructions
|
|
793
|
+
Extract skills, agents (with hierarchy), and tools from this codebase. Return JSON:
|
|
794
|
+
|
|
795
|
+
{
|
|
796
|
+
"skills": [
|
|
797
|
+
{
|
|
798
|
+
"name": "skill-name",
|
|
799
|
+
"description": "When and how to use this pattern",
|
|
800
|
+
"sourceDir": "src/auth",
|
|
801
|
+
"patterns": ["pattern 1", "pattern 2"],
|
|
802
|
+
"triggers": ["keyword1", "keyword2"],
|
|
803
|
+
"category": "architecture|reliability|quality|security|patterns",
|
|
804
|
+
"examples": ["example code snippet or reference"]
|
|
805
|
+
}
|
|
806
|
+
],
|
|
807
|
+
"agents": [
|
|
808
|
+
{
|
|
809
|
+
"name": "root",
|
|
810
|
+
"description": "Main agent for the repository",
|
|
811
|
+
"skills": ["overview-skill"],
|
|
812
|
+
"tools": [{"name": "build", "command": "npm run build", "description": "Build the project"}],
|
|
813
|
+
"isSubAgent": false,
|
|
814
|
+
"subAgents": ["backend", "frontend"],
|
|
815
|
+
"sourceDir": "",
|
|
816
|
+
"triggers": ["main", "primary"]
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
"name": "backend",
|
|
820
|
+
"description": "Backend domain agent",
|
|
821
|
+
"skills": ["api-patterns", "db-access"],
|
|
822
|
+
"tools": [],
|
|
823
|
+
"isSubAgent": true,
|
|
824
|
+
"parentAgent": "root",
|
|
825
|
+
"subAgents": ["auth"],
|
|
826
|
+
"sourceDir": "src/backend",
|
|
827
|
+
"triggers": ["backend", "server", "api"]
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
"name": "auth",
|
|
831
|
+
"description": "Authentication sub-agent",
|
|
832
|
+
"skills": ["oauth-flow"],
|
|
833
|
+
"tools": [],
|
|
834
|
+
"isSubAgent": true,
|
|
835
|
+
"parentAgent": "backend",
|
|
836
|
+
"sourceDir": "src/backend/auth",
|
|
837
|
+
"triggers": ["auth", "login", "oauth"]
|
|
838
|
+
}
|
|
839
|
+
],
|
|
840
|
+
"summary": "One paragraph describing the repository's architecture and purpose"
|
|
841
|
+
}`;
|
|
842
|
+
}
|
|
843
|
+
function parseAnalysisResponse(response, fallbackFn) {
|
|
844
|
+
try {
|
|
845
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
846
|
+
if (!jsonMatch) {
|
|
847
|
+
throw new Error("No JSON found in response");
|
|
848
|
+
}
|
|
849
|
+
const raw = JSON.parse(jsonMatch[0]);
|
|
850
|
+
const validated = validateAnalysisOutput(raw);
|
|
851
|
+
if (!validated) {
|
|
852
|
+
console.warn(" [Zod] Validation failed, falling back");
|
|
853
|
+
return fallbackFn();
|
|
854
|
+
}
|
|
855
|
+
return {
|
|
856
|
+
skills: validated.skills,
|
|
857
|
+
agents: validated.agents,
|
|
858
|
+
hooks: validated.hooks ?? [],
|
|
859
|
+
summary: validated.summary
|
|
860
|
+
};
|
|
861
|
+
} catch {
|
|
862
|
+
return fallbackFn();
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
function generateDefaultSkills(sourceDirectories) {
|
|
866
|
+
return sourceDirectories.map((dir) => ({
|
|
867
|
+
name: `${dir}-patterns`,
|
|
868
|
+
description: `Patterns and conventions from the ${dir} directory`,
|
|
869
|
+
sourceDir: dir,
|
|
870
|
+
patterns: [],
|
|
871
|
+
triggers: [dir],
|
|
872
|
+
category: "patterns",
|
|
873
|
+
examples: []
|
|
874
|
+
}));
|
|
875
|
+
}
|
|
876
|
+
function getDefaultTools(language) {
|
|
877
|
+
if (language === "TypeScript" || language === "JavaScript") {
|
|
878
|
+
return [
|
|
879
|
+
{ name: "install", command: "npm install", description: "Install dependencies" },
|
|
880
|
+
{ name: "build", command: "npm run build", description: "Build" },
|
|
881
|
+
{ name: "test", command: "npm test", description: "Run tests" }
|
|
882
|
+
];
|
|
883
|
+
} else if (language === "Python") {
|
|
884
|
+
return [
|
|
885
|
+
{ name: "install", command: "pip install -e .", description: "Install" },
|
|
886
|
+
{ name: "test", command: "pytest", description: "Run tests" }
|
|
887
|
+
];
|
|
888
|
+
} else if (language === "Go") {
|
|
889
|
+
return [
|
|
890
|
+
{ name: "build", command: "go build ./...", description: "Build" },
|
|
891
|
+
{ name: "test", command: "go test ./...", description: "Run tests" }
|
|
892
|
+
];
|
|
893
|
+
}
|
|
894
|
+
return [];
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/analyzer/local.ts
|
|
898
|
+
init_esm_shims();
|
|
899
|
+
import { CopilotClient, approveAll } from "@github/copilot-sdk";
|
|
900
|
+
import fs2 from "fs/promises";
|
|
901
|
+
import path3 from "path";
|
|
902
|
+
var Analyzer = class {
|
|
903
|
+
verbose;
|
|
904
|
+
client = null;
|
|
905
|
+
constructor(verbose = false) {
|
|
906
|
+
this.verbose = verbose;
|
|
907
|
+
}
|
|
908
|
+
async analyze(scanResult) {
|
|
909
|
+
if (this.verbose) {
|
|
910
|
+
console.log(" [SDK] Initializing CopilotClient...");
|
|
911
|
+
}
|
|
912
|
+
this.client = new CopilotClient({
|
|
913
|
+
logLevel: this.verbose ? "debug" : "error"
|
|
914
|
+
});
|
|
915
|
+
if (this.verbose) {
|
|
916
|
+
console.log(" [SDK] Starting client...");
|
|
917
|
+
}
|
|
918
|
+
try {
|
|
919
|
+
await this.client.start();
|
|
920
|
+
} catch (error) {
|
|
921
|
+
console.error(" [SDK] Failed to start client:", error.message);
|
|
922
|
+
console.error(" [SDK] Make sure Copilot CLI is installed and in PATH");
|
|
923
|
+
console.error(" [SDK] Falling back to heuristic analysis...\n");
|
|
924
|
+
return this.generateFallbackAnalysis(scanResult);
|
|
925
|
+
}
|
|
926
|
+
if (this.verbose) {
|
|
927
|
+
console.log(" [SDK] Client started successfully");
|
|
928
|
+
}
|
|
929
|
+
try {
|
|
930
|
+
if (this.verbose) {
|
|
931
|
+
console.log(" [SDK] Creating session with model: gpt-5...");
|
|
932
|
+
}
|
|
933
|
+
const domains = detectDomainBoundaries(scanResult.files, path3.sep);
|
|
934
|
+
const session = await this.client.createSession({
|
|
935
|
+
model: "gpt-5",
|
|
936
|
+
streaming: true,
|
|
937
|
+
systemMessage: {
|
|
938
|
+
content: getSystemPrompt(scanResult.language, domains)
|
|
939
|
+
},
|
|
940
|
+
onPermissionRequest: approveAll
|
|
941
|
+
});
|
|
942
|
+
if (this.verbose) {
|
|
943
|
+
console.log(" [SDK] Session created successfully");
|
|
944
|
+
}
|
|
945
|
+
const samples = await this.gatherFileSamples(scanResult);
|
|
946
|
+
if (this.verbose) {
|
|
947
|
+
console.log(` [SDK] Gathered ${samples.size} file samples`);
|
|
948
|
+
}
|
|
949
|
+
const fileList = scanResult.files.slice(0, 100).map((f) => f.relativePath).join("\n");
|
|
950
|
+
let sampleContent = "";
|
|
951
|
+
for (const [filePath, content] of samples) {
|
|
952
|
+
sampleContent += `
|
|
953
|
+
--- ${filePath} ---
|
|
954
|
+
${content}
|
|
955
|
+
`;
|
|
956
|
+
}
|
|
957
|
+
const analysisPrompt = buildAnalysisPrompt(
|
|
958
|
+
scanResult.language,
|
|
959
|
+
scanResult.framework,
|
|
960
|
+
scanResult.sourceDirectories,
|
|
961
|
+
scanResult.configFiles,
|
|
962
|
+
fileList,
|
|
963
|
+
sampleContent
|
|
964
|
+
);
|
|
965
|
+
if (this.verbose) {
|
|
966
|
+
console.log(` [SDK] Sending prompt (${analysisPrompt.length} chars)...`);
|
|
967
|
+
}
|
|
968
|
+
let responseContent = "";
|
|
969
|
+
let eventCount = 0;
|
|
970
|
+
const done = new Promise((resolve, reject) => {
|
|
971
|
+
const timeout = setTimeout(() => {
|
|
972
|
+
console.error(`
|
|
973
|
+
[SDK] Timeout after 120s. Events received: ${eventCount}`);
|
|
974
|
+
reject(new Error("SDK timeout"));
|
|
975
|
+
}, 12e4);
|
|
976
|
+
session.on((event) => {
|
|
977
|
+
eventCount++;
|
|
978
|
+
const eventType = event.type;
|
|
979
|
+
const eventData = event.data;
|
|
980
|
+
if (this.verbose && eventType !== "assistant.message_delta") {
|
|
981
|
+
console.log(` [SDK] Event: ${eventType}`);
|
|
982
|
+
}
|
|
983
|
+
if (eventType === "assistant.message") {
|
|
984
|
+
responseContent = eventData.content || "";
|
|
985
|
+
if (this.verbose) {
|
|
986
|
+
console.log(` [SDK] Got final message (${responseContent.length} chars)`);
|
|
987
|
+
}
|
|
988
|
+
} else if (eventType === "assistant.message_delta") {
|
|
989
|
+
process.stdout.write(eventData.deltaContent || "");
|
|
990
|
+
} else if (eventType === "session.idle") {
|
|
991
|
+
clearTimeout(timeout);
|
|
992
|
+
if (this.verbose) {
|
|
993
|
+
console.log(` [SDK] Session idle. Total events: ${eventCount}`);
|
|
994
|
+
}
|
|
995
|
+
resolve();
|
|
996
|
+
} else if (eventType === "error") {
|
|
997
|
+
clearTimeout(timeout);
|
|
998
|
+
console.error(" [SDK] Error event:", eventData);
|
|
999
|
+
reject(new Error("SDK error event"));
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
await session.send({ prompt: analysisPrompt });
|
|
1004
|
+
if (this.verbose) {
|
|
1005
|
+
console.log(" [SDK] Prompt sent, waiting for response...");
|
|
1006
|
+
}
|
|
1007
|
+
await done;
|
|
1008
|
+
if (this.verbose) {
|
|
1009
|
+
console.log("\n");
|
|
1010
|
+
}
|
|
1011
|
+
const result = this.buildResult(responseContent, scanResult);
|
|
1012
|
+
await session.destroy();
|
|
1013
|
+
return result;
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
console.error(`
|
|
1016
|
+
[SDK] Error: ${error.message}`);
|
|
1017
|
+
console.error(" [SDK] Falling back to heuristic analysis...\n");
|
|
1018
|
+
return this.generateFallbackAnalysis(scanResult);
|
|
1019
|
+
} finally {
|
|
1020
|
+
if (this.client) {
|
|
1021
|
+
try {
|
|
1022
|
+
await this.client.stop();
|
|
1023
|
+
} catch {
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
async gatherFileSamples(scanResult) {
|
|
1029
|
+
const samples = /* @__PURE__ */ new Map();
|
|
1030
|
+
const maxSamples = 20;
|
|
1031
|
+
const maxFileSize = 1e4;
|
|
1032
|
+
const priorityFiles = scanResult.files.filter((f) => !f.isTest).sort((a, b) => {
|
|
1033
|
+
if (a.isConfig && !b.isConfig) return -1;
|
|
1034
|
+
if (!a.isConfig && b.isConfig) return 1;
|
|
1035
|
+
const depthA = a.relativePath.split(path3.sep).length;
|
|
1036
|
+
const depthB = b.relativePath.split(path3.sep).length;
|
|
1037
|
+
return depthA - depthB;
|
|
1038
|
+
}).slice(0, maxSamples);
|
|
1039
|
+
for (const file of priorityFiles) {
|
|
1040
|
+
if (file.size > maxFileSize) continue;
|
|
1041
|
+
try {
|
|
1042
|
+
const content = await fs2.readFile(file.path, "utf-8");
|
|
1043
|
+
samples.set(file.relativePath, content.slice(0, maxFileSize));
|
|
1044
|
+
} catch {
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return samples;
|
|
1048
|
+
}
|
|
1049
|
+
buildResult(response, scanResult) {
|
|
1050
|
+
const parsed = parseAnalysisResponse(response, () => null);
|
|
1051
|
+
if (parsed === null) {
|
|
1052
|
+
return this.generateFallbackAnalysis(scanResult);
|
|
1053
|
+
}
|
|
1054
|
+
const flatAgents = flattenAgents(parsed.agents);
|
|
1055
|
+
return {
|
|
1056
|
+
repoName: path3.basename(scanResult.rootPath),
|
|
1057
|
+
skills: parsed.skills,
|
|
1058
|
+
agents: flatAgents,
|
|
1059
|
+
tools: extractAllTools(flatAgents),
|
|
1060
|
+
hooks: parsed.hooks.length > 0 ? parsed.hooks : generateDefaultHooks(scanResult.language, scanResult.testFiles.length > 0),
|
|
1061
|
+
summary: parsed.summary
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
generateFallbackAnalysis(scanResult) {
|
|
1065
|
+
const domains = detectDomainBoundaries(scanResult.files, path3.sep);
|
|
1066
|
+
const skills = generateDefaultSkills(scanResult.sourceDirectories);
|
|
1067
|
+
const tools = detectToolsFromConfig(scanResult.language, scanResult.configFiles);
|
|
1068
|
+
const agents = [];
|
|
1069
|
+
const subAgentNames = [];
|
|
1070
|
+
for (const domain of domains) {
|
|
1071
|
+
const domainSkills = skills.filter(
|
|
1072
|
+
(s) => s.sourceDir === domain.path || s.sourceDir.startsWith(domain.path + path3.sep)
|
|
1073
|
+
);
|
|
1074
|
+
subAgentNames.push(domain.name);
|
|
1075
|
+
agents.push({
|
|
1076
|
+
name: domain.name,
|
|
1077
|
+
description: `Agent for the ${domain.name} domain (${domain.fileCount} files)`,
|
|
1078
|
+
skills: domainSkills.map((s) => s.name),
|
|
1079
|
+
tools: [],
|
|
1080
|
+
isSubAgent: true,
|
|
1081
|
+
parentAgent: "root",
|
|
1082
|
+
sourceDir: domain.path,
|
|
1083
|
+
triggers: [domain.name.toLowerCase()]
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
const rootSkills = skills.filter(
|
|
1087
|
+
(s) => !domains.some((d) => s.sourceDir.startsWith(d.path))
|
|
1088
|
+
);
|
|
1089
|
+
agents.unshift({
|
|
1090
|
+
name: "root",
|
|
1091
|
+
description: `Root agent for this ${scanResult.language} repository`,
|
|
1092
|
+
skills: rootSkills.map((s) => s.name),
|
|
1093
|
+
tools,
|
|
1094
|
+
isSubAgent: false,
|
|
1095
|
+
subAgents: subAgentNames,
|
|
1096
|
+
triggers: [scanResult.language.toLowerCase(), "main", "primary"]
|
|
1097
|
+
});
|
|
1098
|
+
const hooks = generateDefaultHooks(scanResult.language, scanResult.testFiles.length > 0);
|
|
1099
|
+
return {
|
|
1100
|
+
repoName: path3.basename(scanResult.rootPath),
|
|
1101
|
+
skills,
|
|
1102
|
+
agents,
|
|
1103
|
+
tools,
|
|
1104
|
+
hooks,
|
|
1105
|
+
summary: `A ${scanResult.language} repository${scanResult.framework ? ` using ${scanResult.framework}` : ""} with ${domains.length} detected domains.`
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
// src/analyzer/remote.ts
|
|
1111
|
+
init_esm_shims();
|
|
1112
|
+
import { CopilotClient as CopilotClient2, approveAll as approveAll2 } from "@github/copilot-sdk";
|
|
1113
|
+
|
|
1114
|
+
// src/github/index.ts
|
|
1115
|
+
init_esm_shims();
|
|
1116
|
+
import { execFile } from "child_process";
|
|
1117
|
+
import { promisify } from "util";
|
|
1118
|
+
var execFileAsync = promisify(execFile);
|
|
1119
|
+
var GitHubApiError = class extends Error {
|
|
1120
|
+
constructor(message, statusCode) {
|
|
1121
|
+
super(message);
|
|
1122
|
+
this.statusCode = statusCode;
|
|
1123
|
+
this.name = "GitHubApiError";
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
var AuthenticationError = class extends GitHubApiError {
|
|
1127
|
+
constructor() {
|
|
1128
|
+
super('GitHub authentication required. Run "gh auth login" first.');
|
|
1129
|
+
this.name = "AuthenticationError";
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
var RateLimitError = class extends GitHubApiError {
|
|
1133
|
+
constructor(retryAfter) {
|
|
1134
|
+
super(
|
|
1135
|
+
`GitHub API rate limit exceeded${retryAfter ? `. Retry after ${retryAfter}s` : ""}`
|
|
1136
|
+
);
|
|
1137
|
+
this.retryAfter = retryAfter;
|
|
1138
|
+
this.name = "RateLimitError";
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
function parseGitHubUrl(url) {
|
|
1142
|
+
let owner;
|
|
1143
|
+
let repo;
|
|
1144
|
+
if (url.includes("github.com")) {
|
|
1145
|
+
const match = url.match(/github\.com[/:]([\w-]+)\/([\w.-]+)/);
|
|
1146
|
+
if (!match) throw new Error(`Invalid GitHub URL: ${url}`);
|
|
1147
|
+
owner = match[1];
|
|
1148
|
+
repo = match[2].replace(/\.git$/, "");
|
|
1149
|
+
} else if (url.includes("/")) {
|
|
1150
|
+
[owner, repo] = url.split("/");
|
|
1151
|
+
} else {
|
|
1152
|
+
throw new Error(`Invalid GitHub URL: ${url}`);
|
|
1153
|
+
}
|
|
1154
|
+
return { owner, repo };
|
|
1155
|
+
}
|
|
1156
|
+
var MAX_RETRIES = 3;
|
|
1157
|
+
var BASE_DELAY_MS = 1e3;
|
|
1158
|
+
var GitHubClient = class {
|
|
1159
|
+
owner;
|
|
1160
|
+
repo;
|
|
1161
|
+
verbose;
|
|
1162
|
+
constructor(url, verbose = false) {
|
|
1163
|
+
const { owner, repo } = parseGitHubUrl(url);
|
|
1164
|
+
this.owner = owner;
|
|
1165
|
+
this.repo = repo;
|
|
1166
|
+
this.verbose = verbose;
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Execute a gh api command (non-blocking, no shell).
|
|
1170
|
+
*
|
|
1171
|
+
* Automatically retries on 429 (rate-limit) responses with exponential
|
|
1172
|
+
* backoff up to MAX_RETRIES times.
|
|
1173
|
+
*/
|
|
1174
|
+
async api(endpoint) {
|
|
1175
|
+
const args = ["api", `repos/${this.owner}/${this.repo}${endpoint}`];
|
|
1176
|
+
if (this.verbose) {
|
|
1177
|
+
console.log(` [GH] gh ${args.join(" ")}`);
|
|
1178
|
+
}
|
|
1179
|
+
let lastError;
|
|
1180
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
1181
|
+
try {
|
|
1182
|
+
const { stdout } = await execFileAsync("gh", args, {
|
|
1183
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1184
|
+
timeout: 3e4
|
|
1185
|
+
});
|
|
1186
|
+
return stdout;
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
lastError = error;
|
|
1189
|
+
const stderr = error.stderr ?? "";
|
|
1190
|
+
const message = error.message ?? "";
|
|
1191
|
+
const combined = `${stderr} ${message}`;
|
|
1192
|
+
if (combined.includes("auth login") || combined.includes("401") || combined.includes("403") || combined.includes("Not logged in")) {
|
|
1193
|
+
throw new AuthenticationError();
|
|
1194
|
+
}
|
|
1195
|
+
if (combined.includes("429") || combined.includes("rate limit")) {
|
|
1196
|
+
const retryMatch = combined.match(/retry.after[:\s]+(\d+)/i);
|
|
1197
|
+
const retryAfter = retryMatch ? parseInt(retryMatch[1], 10) : void 0;
|
|
1198
|
+
if (attempt < MAX_RETRIES) {
|
|
1199
|
+
const delay = retryAfter ? retryAfter * 1e3 : BASE_DELAY_MS * Math.pow(2, attempt);
|
|
1200
|
+
if (this.verbose) {
|
|
1201
|
+
console.log(
|
|
1202
|
+
` [GH] Rate limited \u2014 retrying in ${delay}ms (attempt ${attempt + 1}/${MAX_RETRIES})`
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
throw new RateLimitError(retryAfter);
|
|
1209
|
+
}
|
|
1210
|
+
throw new GitHubApiError(
|
|
1211
|
+
`GitHub API call failed: ${message || stderr}`
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
throw lastError ?? new GitHubApiError("GitHub API call failed");
|
|
1216
|
+
}
|
|
1217
|
+
// -----------------------------------------------------------------------
|
|
1218
|
+
// Public API (unchanged signatures)
|
|
1219
|
+
// -----------------------------------------------------------------------
|
|
1220
|
+
/**
|
|
1221
|
+
* Get repository metadata
|
|
1222
|
+
*/
|
|
1223
|
+
async getRepoInfo() {
|
|
1224
|
+
const data = JSON.parse(await this.api(""));
|
|
1225
|
+
return {
|
|
1226
|
+
owner: this.owner,
|
|
1227
|
+
repo: this.repo,
|
|
1228
|
+
defaultBranch: data.default_branch,
|
|
1229
|
+
license: data.license?.spdx_id
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Get the file tree (recursive)
|
|
1234
|
+
*/
|
|
1235
|
+
async getTree(branch) {
|
|
1236
|
+
const ref = branch || (await this.getRepoInfo()).defaultBranch;
|
|
1237
|
+
const data = JSON.parse(await this.api(`/git/trees/${ref}?recursive=1`));
|
|
1238
|
+
return data.tree.filter((item) => item.type === "blob" || item.type === "tree").map((item) => ({
|
|
1239
|
+
path: item.path,
|
|
1240
|
+
type: item.type === "blob" ? "file" : "dir",
|
|
1241
|
+
size: item.size,
|
|
1242
|
+
sha: item.sha
|
|
1243
|
+
}));
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Get file content by path
|
|
1247
|
+
*/
|
|
1248
|
+
async getFileContent(path10) {
|
|
1249
|
+
try {
|
|
1250
|
+
const data = JSON.parse(await this.api(`/contents/${path10}`));
|
|
1251
|
+
if (data.encoding === "base64") {
|
|
1252
|
+
return Buffer.from(data.content, "base64").toString("utf-8");
|
|
1253
|
+
}
|
|
1254
|
+
return data.content || "";
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
if (this.verbose) {
|
|
1257
|
+
console.log(
|
|
1258
|
+
` [GH] Failed to fetch ${path10}: ${error.message}`
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
return "";
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Get multiple files in parallel
|
|
1266
|
+
*/
|
|
1267
|
+
async getFiles(paths) {
|
|
1268
|
+
const results = /* @__PURE__ */ new Map();
|
|
1269
|
+
const batchSize = 10;
|
|
1270
|
+
for (let i = 0; i < paths.length; i += batchSize) {
|
|
1271
|
+
const batch = paths.slice(i, i + batchSize);
|
|
1272
|
+
const contents = await Promise.all(
|
|
1273
|
+
batch.map((p) => this.getFileContent(p))
|
|
1274
|
+
);
|
|
1275
|
+
batch.forEach((p, idx) => results.set(p, contents[idx]));
|
|
1276
|
+
}
|
|
1277
|
+
return results;
|
|
1278
|
+
}
|
|
1279
|
+
get fullName() {
|
|
1280
|
+
return `${this.owner}/${this.repo}`;
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
// src/analyzer/remote.ts
|
|
1285
|
+
var IGNORE_PATTERNS2 = [
|
|
1286
|
+
/^node_modules\//,
|
|
1287
|
+
/^\.git\//,
|
|
1288
|
+
/^dist\//,
|
|
1289
|
+
/^build\//,
|
|
1290
|
+
/^\.next\//,
|
|
1291
|
+
/^coverage\//,
|
|
1292
|
+
/^__pycache__\//,
|
|
1293
|
+
/^\.venv\//,
|
|
1294
|
+
/^venv\//,
|
|
1295
|
+
/\.lock$/,
|
|
1296
|
+
/package-lock\.json$/,
|
|
1297
|
+
/yarn\.lock$/,
|
|
1298
|
+
/pnpm-lock\.yaml$/
|
|
1299
|
+
];
|
|
1300
|
+
var CONFIG_FILES = [
|
|
1301
|
+
"package.json",
|
|
1302
|
+
"tsconfig.json",
|
|
1303
|
+
"pyproject.toml",
|
|
1304
|
+
"setup.py",
|
|
1305
|
+
"go.mod",
|
|
1306
|
+
"Cargo.toml",
|
|
1307
|
+
"README.md"
|
|
1308
|
+
];
|
|
1309
|
+
var RemoteAnalyzer = class {
|
|
1310
|
+
verbose;
|
|
1311
|
+
github;
|
|
1312
|
+
constructor(repoUrl, verbose = false) {
|
|
1313
|
+
this.verbose = verbose;
|
|
1314
|
+
this.github = new GitHubClient(repoUrl, verbose);
|
|
1315
|
+
}
|
|
1316
|
+
async analyze() {
|
|
1317
|
+
if (this.verbose) {
|
|
1318
|
+
console.log(` [GH] Fetching repo info for ${this.github.fullName}...`);
|
|
1319
|
+
}
|
|
1320
|
+
const repoInfo = await this.github.getRepoInfo();
|
|
1321
|
+
const tree = await this.github.getTree();
|
|
1322
|
+
if (this.verbose) {
|
|
1323
|
+
console.log(` [GH] Found ${tree.length} files/dirs`);
|
|
1324
|
+
}
|
|
1325
|
+
const files = tree.filter(
|
|
1326
|
+
(f) => f.type === "file" && !IGNORE_PATTERNS2.some((p) => p.test(f.path))
|
|
1327
|
+
);
|
|
1328
|
+
const language = this.detectLanguage(files);
|
|
1329
|
+
const framework = this.detectFramework(files);
|
|
1330
|
+
if (this.verbose) {
|
|
1331
|
+
console.log(` [GH] Language: ${language}, Framework: ${framework || "none"}`);
|
|
1332
|
+
}
|
|
1333
|
+
const priorityPaths = this.selectPriorityFiles(files);
|
|
1334
|
+
if (this.verbose) {
|
|
1335
|
+
console.log(` [GH] Fetching ${priorityPaths.length} priority files...`);
|
|
1336
|
+
}
|
|
1337
|
+
const fileContents = await this.github.getFiles(priorityPaths);
|
|
1338
|
+
const prompt = this.buildPrompt(files, fileContents, language, framework);
|
|
1339
|
+
if (this.verbose) {
|
|
1340
|
+
console.log(` [SDK] Prompt size: ${prompt.length} chars`);
|
|
1341
|
+
}
|
|
1342
|
+
const client = new CopilotClient2({
|
|
1343
|
+
logLevel: this.verbose ? "debug" : "error"
|
|
1344
|
+
});
|
|
1345
|
+
try {
|
|
1346
|
+
if (this.verbose) {
|
|
1347
|
+
console.log(" [SDK] Starting client...");
|
|
1348
|
+
}
|
|
1349
|
+
await client.start();
|
|
1350
|
+
if (this.verbose) {
|
|
1351
|
+
console.log(" [SDK] Client started, state:", client.getState());
|
|
1352
|
+
console.log(" [SDK] Creating session...");
|
|
1353
|
+
}
|
|
1354
|
+
const session = await client.createSession({
|
|
1355
|
+
model: "gpt-5",
|
|
1356
|
+
streaming: true,
|
|
1357
|
+
systemMessage: {
|
|
1358
|
+
content: this.getSystemPrompt()
|
|
1359
|
+
},
|
|
1360
|
+
onPermissionRequest: approveAll2
|
|
1361
|
+
});
|
|
1362
|
+
if (this.verbose) {
|
|
1363
|
+
console.log(` [SDK] Session created: ${session.sessionId}`);
|
|
1364
|
+
}
|
|
1365
|
+
let responseContent = "";
|
|
1366
|
+
let streamedContent = "";
|
|
1367
|
+
let eventCount = 0;
|
|
1368
|
+
const done = new Promise((resolve) => {
|
|
1369
|
+
const timeout = setTimeout(() => {
|
|
1370
|
+
if (this.verbose) {
|
|
1371
|
+
console.log(`
|
|
1372
|
+
[SDK] Session timeout - using streamed content (${streamedContent.length} chars)`);
|
|
1373
|
+
}
|
|
1374
|
+
resolve();
|
|
1375
|
+
}, 12e4);
|
|
1376
|
+
session.on((event) => {
|
|
1377
|
+
eventCount++;
|
|
1378
|
+
const eventType = event.type;
|
|
1379
|
+
const eventData = event.data;
|
|
1380
|
+
if (eventType === "assistant.message_delta") {
|
|
1381
|
+
const delta = eventData.deltaContent || "";
|
|
1382
|
+
streamedContent += delta;
|
|
1383
|
+
process.stdout.write(delta);
|
|
1384
|
+
} else if (eventType === "assistant.message") {
|
|
1385
|
+
responseContent = eventData.content || "";
|
|
1386
|
+
clearTimeout(timeout);
|
|
1387
|
+
resolve();
|
|
1388
|
+
} else if (eventType === "session.idle") {
|
|
1389
|
+
clearTimeout(timeout);
|
|
1390
|
+
resolve();
|
|
1391
|
+
} else if (eventType === "error") {
|
|
1392
|
+
clearTimeout(timeout);
|
|
1393
|
+
console.error(" [SDK] Error event:", eventData);
|
|
1394
|
+
resolve();
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
});
|
|
1398
|
+
if (this.verbose) {
|
|
1399
|
+
console.log(" [SDK] Sending prompt...");
|
|
1400
|
+
}
|
|
1401
|
+
await session.send({ prompt });
|
|
1402
|
+
if (this.verbose) {
|
|
1403
|
+
console.log(" [SDK] Prompt sent, waiting...");
|
|
1404
|
+
}
|
|
1405
|
+
await done;
|
|
1406
|
+
console.log("\n");
|
|
1407
|
+
await session.destroy();
|
|
1408
|
+
await client.stop();
|
|
1409
|
+
const finalContent = responseContent || streamedContent;
|
|
1410
|
+
return this.buildResult(finalContent, repoInfo, language, framework);
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
console.error(` [SDK] Error: ${error.message}`);
|
|
1413
|
+
await client.stop().catch(() => {
|
|
1414
|
+
});
|
|
1415
|
+
return this.generateFallback(repoInfo, language, framework, files);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
detectLanguage(files) {
|
|
1419
|
+
const extCounts = {};
|
|
1420
|
+
const extMap = {
|
|
1421
|
+
".ts": "TypeScript",
|
|
1422
|
+
".tsx": "TypeScript",
|
|
1423
|
+
".js": "JavaScript",
|
|
1424
|
+
".jsx": "JavaScript",
|
|
1425
|
+
".py": "Python",
|
|
1426
|
+
".go": "Go",
|
|
1427
|
+
".rs": "Rust",
|
|
1428
|
+
".java": "Java",
|
|
1429
|
+
".cs": "C#",
|
|
1430
|
+
".rb": "Ruby"
|
|
1431
|
+
};
|
|
1432
|
+
for (const file of files) {
|
|
1433
|
+
const ext = "." + file.path.split(".").pop();
|
|
1434
|
+
if (extMap[ext]) {
|
|
1435
|
+
extCounts[ext] = (extCounts[ext] || 0) + 1;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
let maxCount = 0;
|
|
1439
|
+
let lang = "Unknown";
|
|
1440
|
+
for (const [ext, count] of Object.entries(extCounts)) {
|
|
1441
|
+
if (count > maxCount) {
|
|
1442
|
+
maxCount = count;
|
|
1443
|
+
lang = extMap[ext];
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (lang === "JavaScript" && files.some((f) => f.path.includes("tsconfig"))) {
|
|
1447
|
+
lang = "TypeScript";
|
|
1448
|
+
}
|
|
1449
|
+
return lang;
|
|
1450
|
+
}
|
|
1451
|
+
detectFramework(files) {
|
|
1452
|
+
const paths = new Set(files.map((f) => f.path));
|
|
1453
|
+
if (paths.has("next.config.js") || paths.has("next.config.mjs")) return "Next.js";
|
|
1454
|
+
if (paths.has("angular.json")) return "Angular";
|
|
1455
|
+
if (paths.has("vue.config.js")) return "Vue";
|
|
1456
|
+
if (paths.has("nuxt.config.ts") || paths.has("nuxt.config.js")) return "Nuxt";
|
|
1457
|
+
return void 0;
|
|
1458
|
+
}
|
|
1459
|
+
selectPriorityFiles(files) {
|
|
1460
|
+
const maxFiles = 15;
|
|
1461
|
+
const maxSize = 5e4;
|
|
1462
|
+
const priority = [];
|
|
1463
|
+
for (const cfg of CONFIG_FILES) {
|
|
1464
|
+
const match = files.find((f) => f.path === cfg || f.path.endsWith("/" + cfg));
|
|
1465
|
+
if (match && (match.size || 0) < maxSize) {
|
|
1466
|
+
priority.push(match.path);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
const sourceFiles = files.filter((f) => !priority.includes(f.path) && (f.size || 0) < maxSize).filter((f) => /\.(ts|js|py|go|rs|java)$/.test(f.path)).sort((a, b) => a.path.split("/").length - b.path.split("/").length);
|
|
1470
|
+
for (const f of sourceFiles) {
|
|
1471
|
+
if (priority.length >= maxFiles) break;
|
|
1472
|
+
priority.push(f.path);
|
|
1473
|
+
}
|
|
1474
|
+
return priority;
|
|
1475
|
+
}
|
|
1476
|
+
getSystemPrompt() {
|
|
1477
|
+
return `You are Agent Smith, an AI designed to assimilate repositories into agent hierarchies.
|
|
1478
|
+
|
|
1479
|
+
Analyze the repository and extract:
|
|
1480
|
+
1. SKILLS - Reusable patterns and capabilities (aim for 5-15 skills per repo)
|
|
1481
|
+
2. AGENTS - A root agent plus NESTED SUB-AGENTS for each major domain/directory
|
|
1482
|
+
3. SUB-AGENTS - Always extract 2-7 sub-agents based on directory structure or domain boundaries
|
|
1483
|
+
4. TOOLS - Commands that can be run (build, test, lint)
|
|
1484
|
+
|
|
1485
|
+
CRITICAL: Sub-agents must be nested objects inside the parent's subAgents array, not just names.
|
|
1486
|
+
Each sub-agent needs: name, description, skills, tools, isSubAgent=true, triggers.
|
|
1487
|
+
|
|
1488
|
+
Respond in valid JSON only. No markdown, no explanation.`;
|
|
1489
|
+
}
|
|
1490
|
+
buildPrompt(files, contents, language, framework) {
|
|
1491
|
+
const fileList = files.slice(0, 100).map((f) => f.path).join("\n");
|
|
1492
|
+
let samples = "";
|
|
1493
|
+
for (const [filePath, content] of contents) {
|
|
1494
|
+
if (content) {
|
|
1495
|
+
samples += `
|
|
1496
|
+
--- ${filePath} ---
|
|
1497
|
+
${content.slice(0, 5e3)}
|
|
1498
|
+
`;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return `Analyze this ${language} repository${framework ? ` using ${framework}` : ""}.
|
|
1502
|
+
|
|
1503
|
+
## Files (first 100)
|
|
1504
|
+
${fileList}
|
|
1505
|
+
|
|
1506
|
+
## File Contents
|
|
1507
|
+
${samples}
|
|
1508
|
+
|
|
1509
|
+
## Instructions
|
|
1510
|
+
Extract 5-15 skills and create a hierarchical agent structure with nested sub-agents.
|
|
1511
|
+
Look at directory structure and create sub-agents for major domains (cmd, api, internal, lib, etc.)
|
|
1512
|
+
|
|
1513
|
+
## Return JSON (sub-agents as NESTED OBJECTS, not strings):
|
|
1514
|
+
{
|
|
1515
|
+
"skills": [
|
|
1516
|
+
{"name": "skill-name", "description": "...", "sourceDir": "src/x", "patterns": ["pattern 1"], "triggers": ["keyword"], "category": "patterns", "examples": ["code example"]}
|
|
1517
|
+
],
|
|
1518
|
+
"agents": [
|
|
1519
|
+
{
|
|
1520
|
+
"name": "root",
|
|
1521
|
+
"description": "Main orchestrator for this repo",
|
|
1522
|
+
"skills": ["skill-1", "skill-2"],
|
|
1523
|
+
"tools": ["go build ./...", "npm test"],
|
|
1524
|
+
"isSubAgent": false,
|
|
1525
|
+
"subAgents": [
|
|
1526
|
+
{
|
|
1527
|
+
"name": "cli-agent",
|
|
1528
|
+
"description": "Handles CLI commands",
|
|
1529
|
+
"skills": ["cli-patterns"],
|
|
1530
|
+
"tools": ["./cmd/app help"],
|
|
1531
|
+
"isSubAgent": true,
|
|
1532
|
+
"triggers": ["cmd", "cli", "commands"]
|
|
1533
|
+
},
|
|
1534
|
+
{
|
|
1535
|
+
"name": "api-agent",
|
|
1536
|
+
"description": "Handles API endpoints",
|
|
1537
|
+
"skills": ["api-patterns"],
|
|
1538
|
+
"tools": ["curl localhost:8080/health"],
|
|
1539
|
+
"isSubAgent": true,
|
|
1540
|
+
"triggers": ["api", "http", "endpoints"]
|
|
1541
|
+
}
|
|
1542
|
+
],
|
|
1543
|
+
"triggers": ["main", "root", "${language.toLowerCase()}"]
|
|
1544
|
+
}
|
|
1545
|
+
],
|
|
1546
|
+
"summary": "One paragraph about this repo"
|
|
1547
|
+
}`;
|
|
1548
|
+
}
|
|
1549
|
+
buildResult(response, repo, language, framework) {
|
|
1550
|
+
const parsed = parseAnalysisResponse(response, () => null);
|
|
1551
|
+
if (parsed === null) {
|
|
1552
|
+
return {
|
|
1553
|
+
repoName: repo.repo,
|
|
1554
|
+
skills: [],
|
|
1555
|
+
agents: [],
|
|
1556
|
+
tools: [],
|
|
1557
|
+
hooks: generateDefaultHooks(language, false),
|
|
1558
|
+
summary: "",
|
|
1559
|
+
repo: { ...repo, language, framework }
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
const flatAgents = flattenAgents(parsed.agents);
|
|
1563
|
+
return {
|
|
1564
|
+
repoName: repo.repo,
|
|
1565
|
+
skills: parsed.skills,
|
|
1566
|
+
agents: flatAgents,
|
|
1567
|
+
tools: extractAllTools(flatAgents),
|
|
1568
|
+
hooks: generateDefaultHooks(language, false),
|
|
1569
|
+
summary: parsed.summary,
|
|
1570
|
+
repo: { ...repo, language, framework }
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
generateFallback(repo, language, framework, files) {
|
|
1574
|
+
const srcDirs = /* @__PURE__ */ new Set();
|
|
1575
|
+
for (const f of files) {
|
|
1576
|
+
const parts = f.path.split("/");
|
|
1577
|
+
if (parts.length > 1 && ["src", "lib", "app", "pkg", "cmd"].includes(parts[0])) {
|
|
1578
|
+
srcDirs.add(parts[0]);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
const skills = generateDefaultSkills(Array.from(srcDirs));
|
|
1582
|
+
const tools = getDefaultTools(language);
|
|
1583
|
+
const agents = [{
|
|
1584
|
+
name: "root",
|
|
1585
|
+
description: `Root agent for ${repo.owner}/${repo.repo}`,
|
|
1586
|
+
skills: skills.map((s) => s.name),
|
|
1587
|
+
tools,
|
|
1588
|
+
isSubAgent: false,
|
|
1589
|
+
subAgents: [],
|
|
1590
|
+
triggers: [language.toLowerCase()]
|
|
1591
|
+
}];
|
|
1592
|
+
return {
|
|
1593
|
+
repoName: repo.repo,
|
|
1594
|
+
skills,
|
|
1595
|
+
agents,
|
|
1596
|
+
tools,
|
|
1597
|
+
hooks: generateDefaultHooks(language, false),
|
|
1598
|
+
summary: `A ${language} repository${framework ? ` using ${framework}` : ""}.`,
|
|
1599
|
+
repo: { ...repo, language, framework }
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
|
|
1604
|
+
// src/generator/index.ts
|
|
1605
|
+
init_esm_shims();
|
|
1606
|
+
import fs3 from "fs/promises";
|
|
1607
|
+
import path4 from "path";
|
|
1608
|
+
|
|
1609
|
+
// src/generator/instructions-writer.ts
|
|
1610
|
+
init_esm_shims();
|
|
1611
|
+
var MANAGED_START = "<!-- agentsmith:managed -->";
|
|
1612
|
+
var MANAGED_END = "<!-- /agentsmith:managed -->";
|
|
1613
|
+
function buildCopilotInstructions(analysis) {
|
|
1614
|
+
const sections = [];
|
|
1615
|
+
sections.push(MANAGED_START);
|
|
1616
|
+
sections.push("# Copilot Instructions");
|
|
1617
|
+
sections.push("");
|
|
1618
|
+
if (analysis.summary) {
|
|
1619
|
+
sections.push(analysis.summary);
|
|
1620
|
+
sections.push("");
|
|
1621
|
+
}
|
|
1622
|
+
sections.push("## Language & Framework");
|
|
1623
|
+
sections.push("");
|
|
1624
|
+
const language = analysis.repo?.language || detectLanguageFromSkills(analysis.skills);
|
|
1625
|
+
const framework = analysis.repo?.framework || null;
|
|
1626
|
+
sections.push(`- **Language:** ${language || "Not detected"}`);
|
|
1627
|
+
sections.push(`- **Framework:** ${framework || "None detected"}`);
|
|
1628
|
+
sections.push("");
|
|
1629
|
+
const conventions = collectConventions(analysis.skills);
|
|
1630
|
+
if (conventions.length > 0) {
|
|
1631
|
+
sections.push("## Coding Conventions");
|
|
1632
|
+
sections.push("");
|
|
1633
|
+
for (const convention of conventions) {
|
|
1634
|
+
sections.push(`- ${convention}`);
|
|
1635
|
+
}
|
|
1636
|
+
sections.push("");
|
|
1637
|
+
}
|
|
1638
|
+
const structureEntries = collectStructure(analysis.skills);
|
|
1639
|
+
if (structureEntries.length > 0) {
|
|
1640
|
+
sections.push("## Project Structure");
|
|
1641
|
+
sections.push("");
|
|
1642
|
+
for (const entry of structureEntries) {
|
|
1643
|
+
sections.push(`- \`${entry.dir}/\` -- ${entry.description}`);
|
|
1644
|
+
}
|
|
1645
|
+
sections.push("");
|
|
1646
|
+
}
|
|
1647
|
+
const testingInfo = collectTestingInfo(analysis.skills, analysis.tools);
|
|
1648
|
+
if (testingInfo.length > 0) {
|
|
1649
|
+
sections.push("## Testing");
|
|
1650
|
+
sections.push("");
|
|
1651
|
+
for (const info of testingInfo) {
|
|
1652
|
+
sections.push(`- ${info}`);
|
|
1653
|
+
}
|
|
1654
|
+
sections.push("");
|
|
1655
|
+
}
|
|
1656
|
+
const toolEntries = collectToolInfo(analysis.tools);
|
|
1657
|
+
if (toolEntries.length > 0) {
|
|
1658
|
+
sections.push("## Build & Tools");
|
|
1659
|
+
sections.push("");
|
|
1660
|
+
for (const tool of toolEntries) {
|
|
1661
|
+
sections.push(`- \`${tool.command}\` -- ${tool.description}`);
|
|
1662
|
+
}
|
|
1663
|
+
sections.push("");
|
|
1664
|
+
}
|
|
1665
|
+
sections.push(MANAGED_END);
|
|
1666
|
+
return sections.join("\n");
|
|
1667
|
+
}
|
|
1668
|
+
function mergeWithExisting(existingContent, managedBlock) {
|
|
1669
|
+
const startIdx = existingContent.indexOf(MANAGED_START);
|
|
1670
|
+
const endIdx = existingContent.indexOf(MANAGED_END);
|
|
1671
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
1672
|
+
const before = existingContent.slice(0, startIdx);
|
|
1673
|
+
const after = existingContent.slice(endIdx + MANAGED_END.length);
|
|
1674
|
+
return before + managedBlock + after;
|
|
1675
|
+
}
|
|
1676
|
+
return managedBlock + "\n\n" + existingContent;
|
|
1677
|
+
}
|
|
1678
|
+
function detectLanguageFromSkills(skills) {
|
|
1679
|
+
for (const skill of skills) {
|
|
1680
|
+
for (const pattern of skill.patterns) {
|
|
1681
|
+
const lower = pattern.toLowerCase();
|
|
1682
|
+
if (lower.includes("typescript") || lower.includes(".ts")) return "TypeScript";
|
|
1683
|
+
if (lower.includes("python") || lower.includes(".py")) return "Python";
|
|
1684
|
+
if (lower.includes("javascript") || lower.includes(".js")) return "JavaScript";
|
|
1685
|
+
if (lower.includes("golang") || lower.includes(".go")) return "Go";
|
|
1686
|
+
if (lower.includes("rust") || lower.includes(".rs")) return "Rust";
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return "Not detected";
|
|
1690
|
+
}
|
|
1691
|
+
function collectConventions(skills) {
|
|
1692
|
+
const conventions = [];
|
|
1693
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1694
|
+
for (const skill of skills) {
|
|
1695
|
+
for (const pattern of skill.patterns) {
|
|
1696
|
+
if (!seen.has(pattern) && conventions.length < 15) {
|
|
1697
|
+
seen.add(pattern);
|
|
1698
|
+
conventions.push(pattern);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
return conventions;
|
|
1703
|
+
}
|
|
1704
|
+
function collectStructure(skills) {
|
|
1705
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1706
|
+
const entries = [];
|
|
1707
|
+
for (const skill of skills) {
|
|
1708
|
+
const dir = skill.sourceDir;
|
|
1709
|
+
if (dir && !seen.has(dir)) {
|
|
1710
|
+
seen.add(dir);
|
|
1711
|
+
entries.push({ dir, description: skill.description });
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
return entries;
|
|
1715
|
+
}
|
|
1716
|
+
function collectTestingInfo(skills, tools) {
|
|
1717
|
+
const info = [];
|
|
1718
|
+
for (const tool of tools) {
|
|
1719
|
+
const cmd = tool.command.toLowerCase();
|
|
1720
|
+
if (cmd.includes("test") || cmd.includes("jest") || cmd.includes("vitest") || cmd.includes("pytest")) {
|
|
1721
|
+
info.push(`Run tests: \`${tool.command}\``);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
for (const skill of skills) {
|
|
1725
|
+
if (skill.category === "quality" || skill.name.includes("test")) {
|
|
1726
|
+
for (const pattern of skill.patterns) {
|
|
1727
|
+
if (info.length < 8) {
|
|
1728
|
+
info.push(pattern);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return info;
|
|
1734
|
+
}
|
|
1735
|
+
function collectToolInfo(tools) {
|
|
1736
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1737
|
+
const result = [];
|
|
1738
|
+
for (const tool of tools) {
|
|
1739
|
+
if (!seen.has(tool.command)) {
|
|
1740
|
+
seen.add(tool.command);
|
|
1741
|
+
result.push(tool);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
return result;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// src/generator/agent-writer.ts
|
|
1748
|
+
init_esm_shims();
|
|
1749
|
+
var BASE_TOOLS = [
|
|
1750
|
+
"codebase",
|
|
1751
|
+
"textSearch",
|
|
1752
|
+
"fileSearch",
|
|
1753
|
+
"readFile",
|
|
1754
|
+
"listDirectory",
|
|
1755
|
+
"usages",
|
|
1756
|
+
"problems",
|
|
1757
|
+
"fetch",
|
|
1758
|
+
"githubRepo",
|
|
1759
|
+
"editFiles",
|
|
1760
|
+
"createFile",
|
|
1761
|
+
"createDirectory",
|
|
1762
|
+
"runInTerminal",
|
|
1763
|
+
"terminalLastCommand",
|
|
1764
|
+
"changes"
|
|
1765
|
+
];
|
|
1766
|
+
function buildRootAgentMd(analysis, agentName, helpers) {
|
|
1767
|
+
const rootAgent = analysis.agents.find((a) => !a.isSubAgent);
|
|
1768
|
+
const description = rootAgent?.description || analysis.summary || "Primary orchestrator for this repository";
|
|
1769
|
+
const subAgents = analysis.agents.filter((a) => a.isSubAgent);
|
|
1770
|
+
const tools = [...BASE_TOOLS, "runSubagent"];
|
|
1771
|
+
const toolsList = `tools: [${tools.map((t) => `'${t}'`).join(", ")}]`;
|
|
1772
|
+
const skillsSection = analysis.skills.length > 0 ? analysis.skills.map(
|
|
1773
|
+
(s) => `- [${helpers.toTitleCase(s.name)}](../skills/${s.name}/SKILL.md): ${s.description}`
|
|
1774
|
+
).join("\n") : "No specific skills documented yet.";
|
|
1775
|
+
let delegationSection = "";
|
|
1776
|
+
if (subAgents.length > 0) {
|
|
1777
|
+
const delegationEntries = subAgents.map((sa) => {
|
|
1778
|
+
const triggerList = sa.triggers.length > 0 ? sa.triggers.join(", ") : "general";
|
|
1779
|
+
return `- **@${sa.name}** \u2014 ${sa.description}. Triggers: ${triggerList}`;
|
|
1780
|
+
}).join("\n");
|
|
1781
|
+
delegationSection = `## Agent Delegation
|
|
1782
|
+
|
|
1783
|
+
This agent coordinates work across specialized sub-agents:
|
|
1784
|
+
|
|
1785
|
+
${delegationEntries}
|
|
1786
|
+
|
|
1787
|
+
When asked about these domains, use \`runSubagent\` to delegate to the appropriate specialist.
|
|
1788
|
+
|
|
1789
|
+
`;
|
|
1790
|
+
}
|
|
1791
|
+
const allTools = /* @__PURE__ */ new Set();
|
|
1792
|
+
for (const agent of analysis.agents) {
|
|
1793
|
+
for (const tool of agent.tools) {
|
|
1794
|
+
allTools.add(tool.command);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
const commandsSection = allTools.size > 0 ? Array.from(allTools).map((cmd) => `- \`${cmd}\``).join("\n") : "- `npm install` / `pip install` / `go build` (as appropriate)";
|
|
1798
|
+
return `---
|
|
1799
|
+
name: ${helpers.toTitleCase(agentName)}
|
|
1800
|
+
description: ${helpers.quoteYamlValue(description)}
|
|
1801
|
+
${toolsList}
|
|
1802
|
+
---
|
|
1803
|
+
|
|
1804
|
+
# ${helpers.toTitleCase(agentName)} Agent
|
|
1805
|
+
|
|
1806
|
+
${description}
|
|
1807
|
+
|
|
1808
|
+
${delegationSection}## Skills
|
|
1809
|
+
|
|
1810
|
+
This agent has knowledge of the following patterns and conventions:
|
|
1811
|
+
|
|
1812
|
+
${skillsSection}
|
|
1813
|
+
|
|
1814
|
+
## Commands
|
|
1815
|
+
|
|
1816
|
+
Common commands for this repository:
|
|
1817
|
+
|
|
1818
|
+
${commandsSection}
|
|
1819
|
+
|
|
1820
|
+
## Instructions
|
|
1821
|
+
|
|
1822
|
+
You are the primary orchestrator for this codebase. When working on tasks:
|
|
1823
|
+
|
|
1824
|
+
1. Use \`#codebase\` to search for relevant code patterns
|
|
1825
|
+
2. Reference the skills above to follow established conventions
|
|
1826
|
+
3. Use \`#textSearch\` to find specific implementations
|
|
1827
|
+
4. Use \`#editFiles\` to make changes that follow detected patterns
|
|
1828
|
+
5. Use \`#runInTerminal\` to execute build, test, and lint commands
|
|
1829
|
+
6. Check \`#problems\` to ensure changes don't introduce errors
|
|
1830
|
+
${subAgents.length > 0 ? "7. Use `runSubagent` to delegate domain-specific work to specialist agents\n" : ""}
|
|
1831
|
+
Always follow the patterns documented in the linked skills when making changes.
|
|
1832
|
+
`;
|
|
1833
|
+
}
|
|
1834
|
+
function buildSubAgentMd(agent, skills, helpers) {
|
|
1835
|
+
const toolsList = `tools: [${BASE_TOOLS.map((t) => `'${t}'`).join(", ")}]`;
|
|
1836
|
+
const relevantSkills = skills.filter((s) => agent.skills.includes(s.name));
|
|
1837
|
+
const skillsSection = relevantSkills.length > 0 ? relevantSkills.map(
|
|
1838
|
+
(s) => `- [${helpers.toTitleCase(s.name)}](../skills/${s.name}/SKILL.md): ${s.description}`
|
|
1839
|
+
).join("\n") : "No specific skills documented yet.";
|
|
1840
|
+
const commandsSection = agent.tools.length > 0 ? agent.tools.map((t) => `- \`${t.command}\``).join("\n") : "- See root agent for repository commands";
|
|
1841
|
+
const sourceHint = agent.sourceDir ? `Focus on files in \`${agent.sourceDir}/\`.` : "";
|
|
1842
|
+
return `---
|
|
1843
|
+
name: ${helpers.toTitleCase(agent.name)}
|
|
1844
|
+
description: ${helpers.quoteYamlValue(agent.description)}
|
|
1845
|
+
${toolsList}
|
|
1846
|
+
---
|
|
1847
|
+
|
|
1848
|
+
# ${helpers.toTitleCase(agent.name)} Agent
|
|
1849
|
+
|
|
1850
|
+
Specialist agent for the ${agent.name} domain.
|
|
1851
|
+
|
|
1852
|
+
${agent.description}
|
|
1853
|
+
|
|
1854
|
+
## Skills
|
|
1855
|
+
|
|
1856
|
+
${skillsSection}
|
|
1857
|
+
|
|
1858
|
+
## Commands
|
|
1859
|
+
|
|
1860
|
+
${commandsSection}
|
|
1861
|
+
|
|
1862
|
+
## Instructions
|
|
1863
|
+
|
|
1864
|
+
You are a specialist in the ${agent.name} domain of this codebase.
|
|
1865
|
+
${sourceHint}
|
|
1866
|
+
|
|
1867
|
+
When working on tasks:
|
|
1868
|
+
|
|
1869
|
+
1. Use \`#codebase\` to search for relevant code patterns in your domain
|
|
1870
|
+
2. Reference the skills above to follow established conventions
|
|
1871
|
+
3. Use \`#textSearch\` to find specific implementations
|
|
1872
|
+
4. Use \`#editFiles\` to make changes that follow detected patterns
|
|
1873
|
+
5. Use \`#runInTerminal\` to execute build, test, and lint commands
|
|
1874
|
+
6. Check \`#problems\` to ensure changes don't introduce errors
|
|
1875
|
+
|
|
1876
|
+
Always follow the patterns documented in the linked skills when making changes.
|
|
1877
|
+
`;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// src/generator/handoff-writer.ts
|
|
1881
|
+
init_esm_shims();
|
|
1882
|
+
function buildHandoffGraph(agents) {
|
|
1883
|
+
const subAgents = agents.filter((a) => a.isSubAgent);
|
|
1884
|
+
const handoffs = subAgents.map((agent) => {
|
|
1885
|
+
const from = agent.parentAgent || "repo-root";
|
|
1886
|
+
return {
|
|
1887
|
+
from,
|
|
1888
|
+
to: agent.name,
|
|
1889
|
+
triggers: agent.triggers.length > 0 ? [...agent.triggers] : [agent.name]
|
|
1890
|
+
};
|
|
1891
|
+
});
|
|
1892
|
+
return { handoffs };
|
|
1893
|
+
}
|
|
1894
|
+
function serializeHandoffGraph(graph) {
|
|
1895
|
+
return JSON.stringify(graph, null, 2) + "\n";
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// src/generator/index.ts
|
|
1899
|
+
var Generator = class {
|
|
1900
|
+
rootPath;
|
|
1901
|
+
dryRun;
|
|
1902
|
+
verbose;
|
|
1903
|
+
noInstructions;
|
|
1904
|
+
singleAgent;
|
|
1905
|
+
constructor(rootPath, dryRun = false, verbose = false, noInstructions = false, singleAgent = false) {
|
|
1906
|
+
this.rootPath = rootPath;
|
|
1907
|
+
this.dryRun = dryRun;
|
|
1908
|
+
this.verbose = verbose;
|
|
1909
|
+
this.noInstructions = noInstructions;
|
|
1910
|
+
this.singleAgent = singleAgent;
|
|
1911
|
+
}
|
|
1912
|
+
async generate(analysis) {
|
|
1913
|
+
const files = [];
|
|
1914
|
+
const skillsDir = path4.join(this.rootPath, ".github", "skills");
|
|
1915
|
+
const agentsDir = path4.join(this.rootPath, ".github", "agents");
|
|
1916
|
+
const hooksDir = path4.join(this.rootPath, ".github", "hooks");
|
|
1917
|
+
if (!this.dryRun) {
|
|
1918
|
+
await fs3.mkdir(skillsDir, { recursive: true });
|
|
1919
|
+
await fs3.mkdir(agentsDir, { recursive: true });
|
|
1920
|
+
await fs3.mkdir(hooksDir, { recursive: true });
|
|
1921
|
+
}
|
|
1922
|
+
for (const skill of analysis.skills) {
|
|
1923
|
+
const skillPath = await this.generateSkill(skill, skillsDir);
|
|
1924
|
+
files.push(skillPath);
|
|
1925
|
+
}
|
|
1926
|
+
if (this.singleAgent) {
|
|
1927
|
+
const agentPath = await this.generateMainAgent(analysis, agentsDir);
|
|
1928
|
+
files.push(agentPath);
|
|
1929
|
+
} else {
|
|
1930
|
+
const hasSubAgents = analysis.agents.some((a) => a.isSubAgent);
|
|
1931
|
+
if (!hasSubAgents) {
|
|
1932
|
+
const agentPath = await this.generateMainAgent(analysis, agentsDir);
|
|
1933
|
+
files.push(agentPath);
|
|
1934
|
+
} else {
|
|
1935
|
+
for (const agent of analysis.agents) {
|
|
1936
|
+
const agentPath = await this.generateAgentFile(agent, analysis, agentsDir);
|
|
1937
|
+
files.push(agentPath);
|
|
1938
|
+
}
|
|
1939
|
+
const handoffPath = await this.generateHandoffs(analysis.agents);
|
|
1940
|
+
files.push(handoffPath);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
if (!this.noInstructions) {
|
|
1944
|
+
const instructionsPath = await this.generateCopilotInstructions(analysis);
|
|
1945
|
+
files.push(instructionsPath);
|
|
1946
|
+
}
|
|
1947
|
+
for (const hook of analysis.hooks) {
|
|
1948
|
+
const hookPath = await this.generateHook(hook, hooksDir);
|
|
1949
|
+
files.push(hookPath);
|
|
1950
|
+
}
|
|
1951
|
+
return { files };
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Generate an individual .agent.md file for a single agent in the constellation.
|
|
1955
|
+
* Root agents get `runSubagent`; sub-agents are leaf specialists.
|
|
1956
|
+
*/
|
|
1957
|
+
async generateAgentFile(agent, analysis, agentsDir) {
|
|
1958
|
+
const helpers = {
|
|
1959
|
+
toTitleCase: this.toTitleCase.bind(this),
|
|
1960
|
+
quoteYamlValue: this.quoteYamlValue.bind(this)
|
|
1961
|
+
};
|
|
1962
|
+
let fileName;
|
|
1963
|
+
let content;
|
|
1964
|
+
if (!agent.isSubAgent) {
|
|
1965
|
+
fileName = `${this.sanitizeAgentName(analysis.repoName)}-root.agent.md`;
|
|
1966
|
+
content = buildRootAgentMd(
|
|
1967
|
+
analysis,
|
|
1968
|
+
`${this.sanitizeAgentName(analysis.repoName)}-root`,
|
|
1969
|
+
helpers
|
|
1970
|
+
);
|
|
1971
|
+
} else {
|
|
1972
|
+
fileName = `${this.sanitizeAgentName(agent.name)}.agent.md`;
|
|
1973
|
+
content = buildSubAgentMd(agent, analysis.skills, helpers);
|
|
1974
|
+
}
|
|
1975
|
+
const mdFile = path4.join(agentsDir, fileName);
|
|
1976
|
+
const relativePath = `.github/agents/${fileName}`;
|
|
1977
|
+
if (!this.dryRun) {
|
|
1978
|
+
await fs3.writeFile(mdFile, content, "utf-8");
|
|
1979
|
+
}
|
|
1980
|
+
return relativePath;
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Generate .github/copilot/handoffs.json with the delegation graph.
|
|
1984
|
+
*/
|
|
1985
|
+
async generateHandoffs(agents) {
|
|
1986
|
+
const copilotDir = path4.join(this.rootPath, ".github", "copilot");
|
|
1987
|
+
const handoffFile = path4.join(copilotDir, "handoffs.json");
|
|
1988
|
+
const relativePath = ".github/copilot/handoffs.json";
|
|
1989
|
+
const graph = buildHandoffGraph(agents);
|
|
1990
|
+
const content = serializeHandoffGraph(graph);
|
|
1991
|
+
if (!this.dryRun) {
|
|
1992
|
+
await fs3.mkdir(copilotDir, { recursive: true });
|
|
1993
|
+
await fs3.writeFile(handoffFile, content, "utf-8");
|
|
1994
|
+
}
|
|
1995
|
+
return relativePath;
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Sanitize a name for use as a filename.
|
|
1999
|
+
*/
|
|
2000
|
+
sanitizeAgentName(name) {
|
|
2001
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, "-") || "repo";
|
|
2002
|
+
}
|
|
2003
|
+
// ---- Preserved v0.3 methods below (unchanged) ----
|
|
2004
|
+
async generateSkill(skill, skillsDir) {
|
|
2005
|
+
const skillDir = path4.join(skillsDir, skill.name);
|
|
2006
|
+
const skillFile = path4.join(skillDir, "SKILL.md");
|
|
2007
|
+
const relativePath = `.github/skills/${skill.name}/SKILL.md`;
|
|
2008
|
+
const content = this.buildSkillMarkdown(skill);
|
|
2009
|
+
if (!this.dryRun) {
|
|
2010
|
+
await fs3.mkdir(skillDir, { recursive: true });
|
|
2011
|
+
await fs3.writeFile(skillFile, content, "utf-8");
|
|
2012
|
+
}
|
|
2013
|
+
return relativePath;
|
|
2014
|
+
}
|
|
2015
|
+
buildSkillMarkdown(skill) {
|
|
2016
|
+
const patterns = skill.patterns.length > 0 ? skill.patterns.map((p) => `- ${p}`).join("\n") : "- Patterns extracted from codebase analysis";
|
|
2017
|
+
const examples = skill.examples.length > 0 ? skill.examples.map((e) => `\`\`\`
|
|
2018
|
+
${e}
|
|
2019
|
+
\`\`\``).join("\n\n") : "See source files in the repository for examples.";
|
|
2020
|
+
return `---
|
|
2021
|
+
name: ${this.quoteYamlValue(skill.name)}
|
|
2022
|
+
description: ${this.quoteYamlValue(skill.description)}
|
|
2023
|
+
---
|
|
2024
|
+
|
|
2025
|
+
# ${this.toTitleCase(skill.name)}
|
|
2026
|
+
|
|
2027
|
+
${skill.description}
|
|
2028
|
+
|
|
2029
|
+
## When to Use
|
|
2030
|
+
|
|
2031
|
+
Use this skill when:
|
|
2032
|
+
|
|
2033
|
+
- Working with code in \`${skill.sourceDir}/\`
|
|
2034
|
+
${skill.triggers.map((t) => `- User mentions "${t}"`).join("\n")}
|
|
2035
|
+
|
|
2036
|
+
## Patterns
|
|
2037
|
+
|
|
2038
|
+
${patterns}
|
|
2039
|
+
|
|
2040
|
+
## Examples
|
|
2041
|
+
|
|
2042
|
+
${examples}
|
|
2043
|
+
|
|
2044
|
+
## Category
|
|
2045
|
+
|
|
2046
|
+
**${skill.category}** - ${this.getCategoryDescription(skill.category)}
|
|
2047
|
+
`;
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* Quote a YAML value if it contains special characters
|
|
2051
|
+
*/
|
|
2052
|
+
quoteYamlValue(value) {
|
|
2053
|
+
if (/[:#{}[\]&*?|>!%@`]/.test(value) || value.startsWith("'") || value.startsWith('"')) {
|
|
2054
|
+
return `"${this.escapeYamlString(value)}"`;
|
|
2055
|
+
}
|
|
2056
|
+
return value;
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Escape a string for use inside YAML double quotes
|
|
2060
|
+
*/
|
|
2061
|
+
escapeYamlString(value) {
|
|
2062
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2063
|
+
}
|
|
2064
|
+
getCategoryDescription(category) {
|
|
2065
|
+
const descriptions = {
|
|
2066
|
+
architecture: "Structural patterns and system design",
|
|
2067
|
+
reliability: "Error handling, recovery, and fault tolerance",
|
|
2068
|
+
quality: "Testing, validation, and code quality",
|
|
2069
|
+
security: "Authentication, authorization, and data protection",
|
|
2070
|
+
patterns: "Common code patterns and conventions"
|
|
2071
|
+
};
|
|
2072
|
+
return descriptions[category] || "General patterns";
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Generate ONE main .agent.md file for the entire repo (v0.3 single-agent mode)
|
|
2076
|
+
* This is the VS Code custom agent format
|
|
2077
|
+
* @see https://code.visualstudio.com/docs/copilot/customization/custom-agents
|
|
2078
|
+
*/
|
|
2079
|
+
async generateMainAgent(analysis, agentsDir) {
|
|
2080
|
+
const agentName = analysis.repoName.toLowerCase().replace(/[^a-z0-9]/g, "-") || "repo";
|
|
2081
|
+
const mdFile = path4.join(agentsDir, `${agentName}.agent.md`);
|
|
2082
|
+
const relativePath = `.github/agents/${agentName}.agent.md`;
|
|
2083
|
+
const content = this.buildMainAgentMd(analysis, agentName);
|
|
2084
|
+
if (!this.dryRun) {
|
|
2085
|
+
await fs3.writeFile(mdFile, content, "utf-8");
|
|
2086
|
+
}
|
|
2087
|
+
return relativePath;
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Build the main .agent.md content (v0.3 single-agent mode)
|
|
2091
|
+
* Single agent that knows about all skills in the repo
|
|
2092
|
+
*/
|
|
2093
|
+
buildMainAgentMd(analysis, agentName) {
|
|
2094
|
+
const rootAgent = analysis.agents.find((a) => !a.isSubAgent);
|
|
2095
|
+
const description = rootAgent?.description || analysis.summary || "AI assistant for this repository";
|
|
2096
|
+
const allTools = /* @__PURE__ */ new Set();
|
|
2097
|
+
for (const agent of analysis.agents) {
|
|
2098
|
+
for (const tool of agent.tools) {
|
|
2099
|
+
allTools.add(tool.command);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
const vsCodeTools = [
|
|
2103
|
+
"codebase",
|
|
2104
|
+
// Semantic code search
|
|
2105
|
+
"textSearch",
|
|
2106
|
+
// Find text in files
|
|
2107
|
+
"fileSearch",
|
|
2108
|
+
// Search files by glob
|
|
2109
|
+
"readFile",
|
|
2110
|
+
// Read file content
|
|
2111
|
+
"listDirectory",
|
|
2112
|
+
// List directory contents
|
|
2113
|
+
"usages",
|
|
2114
|
+
// Find references/implementations
|
|
2115
|
+
"problems",
|
|
2116
|
+
// Workspace issues
|
|
2117
|
+
"fetch",
|
|
2118
|
+
// Fetch web pages
|
|
2119
|
+
"githubRepo",
|
|
2120
|
+
// Search GitHub repos
|
|
2121
|
+
"editFiles",
|
|
2122
|
+
// Apply edits to files
|
|
2123
|
+
"createFile",
|
|
2124
|
+
// Create new files
|
|
2125
|
+
"createDirectory",
|
|
2126
|
+
// Create directories
|
|
2127
|
+
"runInTerminal",
|
|
2128
|
+
// Run shell commands
|
|
2129
|
+
"terminalLastCommand",
|
|
2130
|
+
// Get last terminal output
|
|
2131
|
+
"changes"
|
|
2132
|
+
// Source control changes
|
|
2133
|
+
];
|
|
2134
|
+
const toolsList = `tools: [${vsCodeTools.map((t) => `'${t}'`).join(", ")}]`;
|
|
2135
|
+
const skillsSection = analysis.skills.length > 0 ? analysis.skills.map((s) => `- [${this.toTitleCase(s.name)}](../skills/${s.name}/SKILL.md): ${s.description}`).join("\n") : "No specific skills documented yet.";
|
|
2136
|
+
const commandsSection = allTools.size > 0 ? Array.from(allTools).map((cmd) => `- \`${cmd}\``).join("\n") : "- `npm install` / `pip install` / `go build` (as appropriate)";
|
|
2137
|
+
return `---
|
|
2138
|
+
name: ${this.toTitleCase(agentName)}
|
|
2139
|
+
description: ${this.quoteYamlValue(description)}
|
|
2140
|
+
${toolsList}
|
|
2141
|
+
---
|
|
2142
|
+
|
|
2143
|
+
# ${this.toTitleCase(agentName)} Agent
|
|
2144
|
+
|
|
2145
|
+
${description}
|
|
2146
|
+
|
|
2147
|
+
## Skills
|
|
2148
|
+
|
|
2149
|
+
This agent has knowledge of the following patterns and conventions:
|
|
2150
|
+
|
|
2151
|
+
${skillsSection}
|
|
2152
|
+
|
|
2153
|
+
## Commands
|
|
2154
|
+
|
|
2155
|
+
Common commands for this repository:
|
|
2156
|
+
|
|
2157
|
+
${commandsSection}
|
|
2158
|
+
|
|
2159
|
+
## Instructions
|
|
2160
|
+
|
|
2161
|
+
You are an AI assistant specialized in this codebase. When working on tasks:
|
|
2162
|
+
|
|
2163
|
+
1. Use \`#codebase\` to search for relevant code patterns
|
|
2164
|
+
2. Reference the skills above to follow established conventions
|
|
2165
|
+
3. Use \`#textSearch\` to find specific implementations
|
|
2166
|
+
4. Use \`#editFiles\` to make changes that follow detected patterns
|
|
2167
|
+
5. Use \`#runInTerminal\` to execute build, test, and lint commands
|
|
2168
|
+
6. Check \`#problems\` to ensure changes don't introduce errors
|
|
2169
|
+
|
|
2170
|
+
Always follow the patterns documented in the linked skills when making changes.
|
|
2171
|
+
`;
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Generate .github/copilot-instructions.md
|
|
2175
|
+
* Supports idempotent re-generation by preserving content outside managed markers.
|
|
2176
|
+
*/
|
|
2177
|
+
async generateCopilotInstructions(analysis) {
|
|
2178
|
+
const githubDir = path4.join(this.rootPath, ".github");
|
|
2179
|
+
const filePath = path4.join(githubDir, "copilot-instructions.md");
|
|
2180
|
+
const relativePath = ".github/copilot-instructions.md";
|
|
2181
|
+
const managedBlock = buildCopilotInstructions(analysis);
|
|
2182
|
+
let finalContent = managedBlock;
|
|
2183
|
+
if (!this.dryRun) {
|
|
2184
|
+
await fs3.mkdir(githubDir, { recursive: true });
|
|
2185
|
+
try {
|
|
2186
|
+
const existing = await fs3.readFile(filePath, "utf-8");
|
|
2187
|
+
finalContent = mergeWithExisting(existing, managedBlock);
|
|
2188
|
+
} catch {
|
|
2189
|
+
}
|
|
2190
|
+
await fs3.writeFile(filePath, finalContent, "utf-8");
|
|
2191
|
+
}
|
|
2192
|
+
return relativePath;
|
|
2193
|
+
}
|
|
2194
|
+
async generateHook(hook, hooksDir) {
|
|
2195
|
+
const hookFile = path4.join(hooksDir, `${hook.name}.yaml`);
|
|
2196
|
+
const relativePath = `.github/hooks/${hook.name}.yaml`;
|
|
2197
|
+
const content = this.buildHookYaml(hook);
|
|
2198
|
+
if (!this.dryRun) {
|
|
2199
|
+
await fs3.writeFile(hookFile, content, "utf-8");
|
|
2200
|
+
}
|
|
2201
|
+
return relativePath;
|
|
2202
|
+
}
|
|
2203
|
+
buildHookYaml(hook) {
|
|
2204
|
+
const commandsList = hook.commands.map((c) => ` - "${c}"`).join("\n");
|
|
2205
|
+
let content = `# Hook Configuration
|
|
2206
|
+
# Generated by Agent Smith
|
|
2207
|
+
# "Never send a human to do a machine's job."
|
|
2208
|
+
|
|
2209
|
+
name: ${hook.name}
|
|
2210
|
+
event: ${hook.event}
|
|
2211
|
+
description: ${hook.description}
|
|
2212
|
+
|
|
2213
|
+
# Commands to execute
|
|
2214
|
+
commands:
|
|
2215
|
+
${commandsList}
|
|
2216
|
+
`;
|
|
2217
|
+
if (hook.condition) {
|
|
2218
|
+
content += `
|
|
2219
|
+
# Condition for hook execution
|
|
2220
|
+
condition: "${hook.condition}"
|
|
2221
|
+
`;
|
|
2222
|
+
}
|
|
2223
|
+
return content;
|
|
2224
|
+
}
|
|
2225
|
+
toTitleCase(str) {
|
|
2226
|
+
if (typeof str !== "string") {
|
|
2227
|
+
if (str && typeof str === "object" && "name" in str) {
|
|
2228
|
+
str = str.name;
|
|
2229
|
+
} else {
|
|
2230
|
+
str = String(str ?? "unknown");
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2234
|
+
}
|
|
2235
|
+
};
|
|
2236
|
+
|
|
2237
|
+
// src/registry/index.ts
|
|
2238
|
+
init_esm_shims();
|
|
2239
|
+
import fs4 from "fs/promises";
|
|
2240
|
+
import path5 from "path";
|
|
2241
|
+
var Registry = class {
|
|
2242
|
+
rootPath;
|
|
2243
|
+
dryRun;
|
|
2244
|
+
registryPath;
|
|
2245
|
+
constructor(rootPath, dryRun = false) {
|
|
2246
|
+
this.rootPath = rootPath;
|
|
2247
|
+
this.dryRun = dryRun;
|
|
2248
|
+
this.registryPath = path5.join(rootPath, "skills-registry.jsonl");
|
|
2249
|
+
}
|
|
2250
|
+
async build(skills, agents) {
|
|
2251
|
+
const entries = [];
|
|
2252
|
+
for (const skill of skills) {
|
|
2253
|
+
entries.push({
|
|
2254
|
+
type: "skill",
|
|
2255
|
+
name: skill.name,
|
|
2256
|
+
file: `.github/skills/${skill.name}/SKILL.md`,
|
|
2257
|
+
description: skill.description,
|
|
2258
|
+
category: skill.category,
|
|
2259
|
+
triggers: skill.triggers
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
if (agents) {
|
|
2263
|
+
for (const agent of agents) {
|
|
2264
|
+
entries.push({
|
|
2265
|
+
type: "agent",
|
|
2266
|
+
name: agent.name,
|
|
2267
|
+
file: `.github/agents/${agent.name}.agent.md`,
|
|
2268
|
+
vsCodeAgent: `.github/agents/${agent.name}.agent.md`,
|
|
2269
|
+
description: agent.description,
|
|
2270
|
+
triggers: agent.triggers,
|
|
2271
|
+
isSubAgent: agent.isSubAgent,
|
|
2272
|
+
parentAgent: agent.parentAgent,
|
|
2273
|
+
subAgents: agent.subAgents
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
const content = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2278
|
+
if (!this.dryRun) {
|
|
2279
|
+
await fs4.writeFile(this.registryPath, content, "utf-8");
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
async search(query, options) {
|
|
2283
|
+
const limit = options?.limit ?? 10;
|
|
2284
|
+
const typeFilter = options?.type;
|
|
2285
|
+
try {
|
|
2286
|
+
const content = await fs4.readFile(this.registryPath, "utf-8");
|
|
2287
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
2288
|
+
let entries = lines.map((line) => JSON.parse(line));
|
|
2289
|
+
if (typeFilter) {
|
|
2290
|
+
entries = entries.filter((e) => e.type === typeFilter);
|
|
2291
|
+
}
|
|
2292
|
+
const queryLower = query.toLowerCase();
|
|
2293
|
+
const scored = entries.map((entry) => {
|
|
2294
|
+
let score = 0;
|
|
2295
|
+
if (entry.name.toLowerCase() === queryLower) score += 100;
|
|
2296
|
+
if (entry.name.toLowerCase().includes(queryLower)) score += 50;
|
|
2297
|
+
if (entry.description.toLowerCase().includes(queryLower)) score += 30;
|
|
2298
|
+
for (const trigger of entry.triggers) {
|
|
2299
|
+
if (trigger.toLowerCase().includes(queryLower)) score += 20;
|
|
2300
|
+
if (trigger.toLowerCase() === queryLower) score += 40;
|
|
2301
|
+
}
|
|
2302
|
+
if (entry.category?.toLowerCase().includes(queryLower)) score += 10;
|
|
2303
|
+
if (entry.type === "agent" && !entry.isSubAgent) score += 5;
|
|
2304
|
+
return { entry, score };
|
|
2305
|
+
});
|
|
2306
|
+
return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((s) => s.entry);
|
|
2307
|
+
} catch (error) {
|
|
2308
|
+
return [];
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
async list() {
|
|
2312
|
+
try {
|
|
2313
|
+
const content = await fs4.readFile(this.registryPath, "utf-8");
|
|
2314
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
2315
|
+
return lines.map((line) => JSON.parse(line));
|
|
2316
|
+
} catch {
|
|
2317
|
+
return [];
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
async get(name) {
|
|
2321
|
+
const entries = await this.list();
|
|
2322
|
+
return entries.find((e) => e.name === name) || null;
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
|
|
2326
|
+
// src/hooks/index.ts
|
|
2327
|
+
init_esm_shims();
|
|
2328
|
+
import fs5 from "fs/promises";
|
|
2329
|
+
import path6 from "path";
|
|
2330
|
+
import { execSync } from "child_process";
|
|
2331
|
+
import yaml from "yaml";
|
|
2332
|
+
import chalk from "chalk";
|
|
2333
|
+
var HookRunner = class {
|
|
2334
|
+
rootPath;
|
|
2335
|
+
verbose;
|
|
2336
|
+
constructor(rootPath, verbose = false) {
|
|
2337
|
+
this.rootPath = rootPath;
|
|
2338
|
+
this.verbose = verbose;
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Execute all hooks for a given event
|
|
2342
|
+
*/
|
|
2343
|
+
async execute(event) {
|
|
2344
|
+
const hooks = await this.loadHooks(event);
|
|
2345
|
+
const results = [];
|
|
2346
|
+
if (hooks.length === 0) {
|
|
2347
|
+
if (this.verbose) {
|
|
2348
|
+
console.log(chalk.gray(` No ${event} hooks found.`));
|
|
2349
|
+
}
|
|
2350
|
+
return results;
|
|
2351
|
+
}
|
|
2352
|
+
console.log(chalk.green(`
|
|
2353
|
+
[HOOKS]`), `Running ${event} hooks...`);
|
|
2354
|
+
for (const hook of hooks) {
|
|
2355
|
+
const result = await this.runHook(hook);
|
|
2356
|
+
results.push(result);
|
|
2357
|
+
if (!result.success) {
|
|
2358
|
+
console.log(chalk.red(` \u2717 ${hook.name}: ${result.error}`));
|
|
2359
|
+
break;
|
|
2360
|
+
} else {
|
|
2361
|
+
console.log(chalk.green(` \u2713 ${hook.name}`));
|
|
2362
|
+
if (this.verbose && result.output) {
|
|
2363
|
+
console.log(chalk.gray(` ${result.output.split("\n").join("\n ")}`));
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
return results;
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Load hooks from .github/hooks/ directory for a specific event
|
|
2371
|
+
*/
|
|
2372
|
+
async loadHooks(event) {
|
|
2373
|
+
const hooksDir = path6.join(this.rootPath, ".github", "hooks");
|
|
2374
|
+
const hooks = [];
|
|
2375
|
+
try {
|
|
2376
|
+
const files = await fs5.readdir(hooksDir);
|
|
2377
|
+
for (const file of files) {
|
|
2378
|
+
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
|
|
2379
|
+
const hookPath = path6.join(hooksDir, file);
|
|
2380
|
+
const content = await fs5.readFile(hookPath, "utf-8");
|
|
2381
|
+
const hookDef = yaml.parse(content);
|
|
2382
|
+
if (hookDef.event === event) {
|
|
2383
|
+
hooks.push(hookDef);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
} catch (error) {
|
|
2387
|
+
if (this.verbose) {
|
|
2388
|
+
console.log(chalk.gray(` No hooks directory found at ${hooksDir}`));
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
return hooks;
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Run a single hook and return the result
|
|
2395
|
+
*/
|
|
2396
|
+
async runHook(hook) {
|
|
2397
|
+
const outputs = [];
|
|
2398
|
+
for (const command of hook.commands) {
|
|
2399
|
+
try {
|
|
2400
|
+
if (hook.condition) {
|
|
2401
|
+
const conditionMet = await this.evaluateCondition(hook.condition);
|
|
2402
|
+
if (!conditionMet) {
|
|
2403
|
+
return {
|
|
2404
|
+
hook: hook.name,
|
|
2405
|
+
success: true,
|
|
2406
|
+
output: "Skipped: condition not met"
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
if (this.verbose) {
|
|
2411
|
+
console.log(chalk.gray(` Running: ${command}`));
|
|
2412
|
+
}
|
|
2413
|
+
const output = execSync(command, {
|
|
2414
|
+
cwd: this.rootPath,
|
|
2415
|
+
encoding: "utf-8",
|
|
2416
|
+
stdio: this.verbose ? "pipe" : "pipe",
|
|
2417
|
+
timeout: 12e4
|
|
2418
|
+
// 2 minute timeout per command
|
|
2419
|
+
});
|
|
2420
|
+
outputs.push(output.trim());
|
|
2421
|
+
} catch (error) {
|
|
2422
|
+
const err = error;
|
|
2423
|
+
return {
|
|
2424
|
+
hook: hook.name,
|
|
2425
|
+
success: false,
|
|
2426
|
+
error: err.stderr || err.message || "Command failed"
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
return {
|
|
2431
|
+
hook: hook.name,
|
|
2432
|
+
success: true,
|
|
2433
|
+
output: outputs.join("\n")
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
/**
|
|
2437
|
+
* Evaluate a condition string (simple file/command checks)
|
|
2438
|
+
*/
|
|
2439
|
+
async evaluateCondition(condition) {
|
|
2440
|
+
if (condition.startsWith("file:")) {
|
|
2441
|
+
const filePath = path6.join(this.rootPath, condition.slice(5));
|
|
2442
|
+
try {
|
|
2443
|
+
await fs5.access(filePath);
|
|
2444
|
+
return true;
|
|
2445
|
+
} catch {
|
|
2446
|
+
return false;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
if (condition.startsWith("command:")) {
|
|
2450
|
+
try {
|
|
2451
|
+
execSync(condition.slice(8), { encoding: "utf-8", stdio: "pipe" });
|
|
2452
|
+
return true;
|
|
2453
|
+
} catch {
|
|
2454
|
+
return false;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return true;
|
|
2458
|
+
}
|
|
2459
|
+
};
|
|
2460
|
+
|
|
2461
|
+
// src/commands/assimilate.ts
|
|
2462
|
+
init_git();
|
|
2463
|
+
init_license();
|
|
2464
|
+
async function assimilateCommand(target, options) {
|
|
2465
|
+
const isRemote = isGitHubUrl(target);
|
|
2466
|
+
if (isRemote) {
|
|
2467
|
+
console.log(chalk2.green("\n[ANALYZE]"), `Analyzing ${getRepoName(target)} via GitHub API...`);
|
|
2468
|
+
const analyzer = new RemoteAnalyzer(target, options.verbose);
|
|
2469
|
+
const result = await analyzer.analyze();
|
|
2470
|
+
if (options.verbose) {
|
|
2471
|
+
console.log(chalk2.gray(` \u251C\u2500\u2500 Language: ${result.repo?.language ?? "Unknown"}`));
|
|
2472
|
+
console.log(chalk2.gray(` \u251C\u2500\u2500 Framework: ${result.repo?.framework || "None"}`));
|
|
2473
|
+
console.log(chalk2.gray(` \u251C\u2500\u2500 License: ${result.repo?.license || "Unknown"}`));
|
|
2474
|
+
console.log(chalk2.gray(` \u2514\u2500\u2500 Skills: ${result.skills.length}`));
|
|
2475
|
+
}
|
|
2476
|
+
console.log(chalk2.green("\n[LICENSE]"), "Checking repository license...");
|
|
2477
|
+
const isPermissive = isPermissiveLicense(result.repo?.license);
|
|
2478
|
+
if (!isPermissive && !options.dryRun) {
|
|
2479
|
+
console.log(chalk2.red("\n[BLOCKED]"), "Cannot assimilate repository.");
|
|
2480
|
+
if (!result.repo?.license) {
|
|
2481
|
+
console.log(chalk2.red(" No license detected."));
|
|
2482
|
+
} else {
|
|
2483
|
+
console.log(chalk2.red(` License "${result.repo.license}" is not permissive.`));
|
|
2484
|
+
}
|
|
2485
|
+
console.log(chalk2.gray(" Use --dry-run to preview without restrictions."));
|
|
2486
|
+
process.exitCode = 1;
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
if (isPermissive) {
|
|
2490
|
+
console.log(chalk2.green(` \u2713 ${result.repo?.license} - permissive license`));
|
|
2491
|
+
} else if (options.dryRun) {
|
|
2492
|
+
console.log(chalk2.yellow(" \u26A0 License not permissive - generation blocked without --dry-run"));
|
|
2493
|
+
}
|
|
2494
|
+
const outputPath = options.output || process.cwd();
|
|
2495
|
+
console.log(
|
|
2496
|
+
chalk2.green("\n[GENERATE]"),
|
|
2497
|
+
options.dryRun ? "Preview of assets..." : `Writing assets to ${outputPath}/.github/...`
|
|
2498
|
+
);
|
|
2499
|
+
const generator = new Generator(
|
|
2500
|
+
outputPath,
|
|
2501
|
+
options.dryRun,
|
|
2502
|
+
options.verbose,
|
|
2503
|
+
options.instructions === false,
|
|
2504
|
+
options.singleAgent
|
|
2505
|
+
);
|
|
2506
|
+
const generated = await generator.generate(result);
|
|
2507
|
+
for (const file of generated.files) {
|
|
2508
|
+
const icon = options.dryRun ? chalk2.yellow("\u25CB") : chalk2.green("\u2713");
|
|
2509
|
+
console.log(` ${icon} ${file}`);
|
|
2510
|
+
}
|
|
2511
|
+
const registry = new Registry(outputPath, options.dryRun);
|
|
2512
|
+
await registry.build(result.skills, result.agents);
|
|
2513
|
+
const registryIcon = options.dryRun ? chalk2.yellow("\u25CB") : chalk2.green("\u2713");
|
|
2514
|
+
console.log(` ${registryIcon} skills-registry.jsonl`);
|
|
2515
|
+
if (!options.dryRun) {
|
|
2516
|
+
const hookRunner = new HookRunner(outputPath, options.verbose);
|
|
2517
|
+
await hookRunner.execute("post-generate");
|
|
2518
|
+
}
|
|
2519
|
+
const agentCount = generated.files.filter((f) => f.endsWith(".agent.md")).length;
|
|
2520
|
+
console.log(
|
|
2521
|
+
chalk2.green("\n[COMPLETE]"),
|
|
2522
|
+
`${result.skills.length} skills, ${agentCount} agent(s), ${result.hooks.length} hooks generated.`
|
|
2523
|
+
);
|
|
2524
|
+
if (options.dryRun) {
|
|
2525
|
+
console.log(chalk2.yellow("\nDry run - no files were written."));
|
|
2526
|
+
} else {
|
|
2527
|
+
console.log(chalk2.gray("\nYour repository has been assimilated.\n"));
|
|
2528
|
+
}
|
|
2529
|
+
} else {
|
|
2530
|
+
await assimilateLocal(target, options);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
async function assimilateLocal(target, options) {
|
|
2534
|
+
const { resolveInput: resolveInput2 } = await Promise.resolve().then(() => (init_git(), git_exports));
|
|
2535
|
+
const { detectLicense: detectLicense2, formatLicenseStatus: formatLicenseStatus2 } = await Promise.resolve().then(() => (init_license(), license_exports));
|
|
2536
|
+
const resolved = await resolveInput2(target);
|
|
2537
|
+
try {
|
|
2538
|
+
console.log(chalk2.green("\n[SCAN]"), "Enumerating repository...");
|
|
2539
|
+
const scanner = new Scanner(resolved.path, options.verbose);
|
|
2540
|
+
const scanResult = await scanner.scan();
|
|
2541
|
+
if (options.verbose) {
|
|
2542
|
+
console.log(chalk2.gray(` \u251C\u2500\u2500 Language: ${scanResult.language}`));
|
|
2543
|
+
console.log(chalk2.gray(` \u251C\u2500\u2500 Framework: ${scanResult.framework || "None detected"}`));
|
|
2544
|
+
console.log(chalk2.gray(` \u251C\u2500\u2500 Files: ${scanResult.files.length}`));
|
|
2545
|
+
console.log(chalk2.gray(` \u2514\u2500\u2500 Config: ${scanResult.configFiles.join(", ") || "None"}`));
|
|
2546
|
+
}
|
|
2547
|
+
console.log(chalk2.green("\n[ANALYZE]"), "Copilot SDK analysis in progress...");
|
|
2548
|
+
const analyzer = new Analyzer(options.verbose);
|
|
2549
|
+
const analysisResult = await analyzer.analyze(scanResult);
|
|
2550
|
+
if (options.verbose) {
|
|
2551
|
+
for (const skill of analysisResult.skills) {
|
|
2552
|
+
console.log(chalk2.gray(` \u251C\u2500\u2500 ${skill.sourceDir} \u2192 ${skill.name}`));
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
console.log(chalk2.green("\n[LICENSE]"), "Checking repository license...");
|
|
2556
|
+
const license = await detectLicense2(resolved.path);
|
|
2557
|
+
if (options.verbose) {
|
|
2558
|
+
console.log(chalk2.gray(` \u2514\u2500\u2500 ${formatLicenseStatus2(license)}`));
|
|
2559
|
+
}
|
|
2560
|
+
if (!license.permissive && !options.dryRun) {
|
|
2561
|
+
console.log(chalk2.red("\n[BLOCKED]"), "Cannot assimilate repository.");
|
|
2562
|
+
if (!license.detected) {
|
|
2563
|
+
console.log(chalk2.red(" No license file found."));
|
|
2564
|
+
} else {
|
|
2565
|
+
console.log(chalk2.red(` License "${license.name}" is not permissive.`));
|
|
2566
|
+
}
|
|
2567
|
+
process.exitCode = 1;
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
if (license.permissive) {
|
|
2571
|
+
console.log(chalk2.green(` \u2713 ${license.name} - permissive license`));
|
|
2572
|
+
}
|
|
2573
|
+
const outputPath = options.output || resolved.path;
|
|
2574
|
+
console.log(
|
|
2575
|
+
chalk2.green("\n[GENERATE]"),
|
|
2576
|
+
options.dryRun ? "Preview of assets..." : `Writing assets to .github/...`
|
|
2577
|
+
);
|
|
2578
|
+
const generator = new Generator(
|
|
2579
|
+
outputPath,
|
|
2580
|
+
options.dryRun,
|
|
2581
|
+
options.verbose,
|
|
2582
|
+
options.instructions === false,
|
|
2583
|
+
options.singleAgent
|
|
2584
|
+
);
|
|
2585
|
+
const generated = await generator.generate(analysisResult);
|
|
2586
|
+
for (const file of generated.files) {
|
|
2587
|
+
const icon = options.dryRun ? chalk2.yellow("\u25CB") : chalk2.green("\u2713");
|
|
2588
|
+
console.log(` ${icon} ${file}`);
|
|
2589
|
+
}
|
|
2590
|
+
const registry = new Registry(outputPath, options.dryRun);
|
|
2591
|
+
await registry.build(analysisResult.skills, analysisResult.agents);
|
|
2592
|
+
console.log(` ${options.dryRun ? chalk2.yellow("\u25CB") : chalk2.green("\u2713")} skills-registry.jsonl`);
|
|
2593
|
+
if (!options.dryRun) {
|
|
2594
|
+
const hookRunner = new HookRunner(outputPath, options.verbose);
|
|
2595
|
+
await hookRunner.execute("post-generate");
|
|
2596
|
+
}
|
|
2597
|
+
const localAgentCount = generated.files.filter((f) => f.endsWith(".agent.md")).length;
|
|
2598
|
+
console.log(
|
|
2599
|
+
chalk2.green("\n[COMPLETE]"),
|
|
2600
|
+
`${analysisResult.skills.length} skills, ${localAgentCount} agent(s), ${analysisResult.hooks.length} hooks generated.`
|
|
2601
|
+
);
|
|
2602
|
+
if (options.dryRun) {
|
|
2603
|
+
console.log(chalk2.yellow("\nDry run - no files were written."));
|
|
2604
|
+
} else {
|
|
2605
|
+
console.log(chalk2.gray("\nYour repository has been assimilated.\n"));
|
|
2606
|
+
}
|
|
2607
|
+
} finally {
|
|
2608
|
+
if (resolved.isTemporary) {
|
|
2609
|
+
await resolved.cleanup();
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// src/commands/search.ts
|
|
2615
|
+
init_esm_shims();
|
|
2616
|
+
import chalk3 from "chalk";
|
|
2617
|
+
async function searchCommand(query, options) {
|
|
2618
|
+
const limit = parseInt(options.limit || "10", 10);
|
|
2619
|
+
const cwd = process.cwd();
|
|
2620
|
+
const registry = new Registry(cwd);
|
|
2621
|
+
const results = await registry.search(query, { type: options.type, limit });
|
|
2622
|
+
if (results.length === 0) {
|
|
2623
|
+
const typeLabel = options.type ? `${options.type}s` : "entries";
|
|
2624
|
+
console.log(chalk3.yellow(`No ${typeLabel} found matching "${query}"`));
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
console.log(
|
|
2628
|
+
chalk3.white("\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510")
|
|
2629
|
+
);
|
|
2630
|
+
console.log(
|
|
2631
|
+
chalk3.white("\u2502 Type \u2502 Name \u2502 Description \u2502")
|
|
2632
|
+
);
|
|
2633
|
+
console.log(
|
|
2634
|
+
chalk3.white("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524")
|
|
2635
|
+
);
|
|
2636
|
+
for (const result of results) {
|
|
2637
|
+
const type = (result.type || "skill").padEnd(6).slice(0, 6);
|
|
2638
|
+
const name = result.name.padEnd(15).slice(0, 15);
|
|
2639
|
+
const desc = result.description.slice(0, 48).padEnd(48);
|
|
2640
|
+
const typeColor = result.type === "agent" ? chalk3.cyan : chalk3.magenta;
|
|
2641
|
+
console.log(chalk3.white(`\u2502 ${typeColor(type)} \u2502 ${name} \u2502 ${desc} \u2502`));
|
|
2642
|
+
}
|
|
2643
|
+
console.log(
|
|
2644
|
+
chalk3.white("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n")
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// src/commands/validate.ts
|
|
2649
|
+
init_esm_shims();
|
|
2650
|
+
import fs8 from "fs/promises";
|
|
2651
|
+
import path9 from "path";
|
|
2652
|
+
import chalk4 from "chalk";
|
|
2653
|
+
import yaml2 from "yaml";
|
|
2654
|
+
async function validateCommand(targetPath = ".", options = {}) {
|
|
2655
|
+
const rootPath = path9.resolve(targetPath);
|
|
2656
|
+
const result = { valid: true, errors: [], warnings: [] };
|
|
2657
|
+
console.log(chalk4.green("\n[VALIDATE]"), "Checking agent assets...\n");
|
|
2658
|
+
await validateSkills(rootPath, result, options.verbose);
|
|
2659
|
+
await validateAgents(rootPath, result, options.verbose);
|
|
2660
|
+
await validateHooks(rootPath, result, options.verbose);
|
|
2661
|
+
await validateRegistry(rootPath, result, options.verbose);
|
|
2662
|
+
console.log("");
|
|
2663
|
+
if (result.errors.length > 0) {
|
|
2664
|
+
console.log(chalk4.red(`
|
|
2665
|
+
\u2717 Validation failed with ${result.errors.length} error(s):`));
|
|
2666
|
+
for (const error of result.errors) {
|
|
2667
|
+
console.log(chalk4.red(` \u2022 ${error}`));
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
if (result.warnings.length > 0) {
|
|
2671
|
+
console.log(chalk4.yellow(`
|
|
2672
|
+
\u26A0 ${result.warnings.length} warning(s):`));
|
|
2673
|
+
for (const warning of result.warnings) {
|
|
2674
|
+
console.log(chalk4.yellow(` \u2022 ${warning}`));
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
if (result.valid) {
|
|
2678
|
+
console.log(chalk4.green("\n\u2713 All agent assets are valid."));
|
|
2679
|
+
} else {
|
|
2680
|
+
process.exitCode = 1;
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
async function validateSkills(rootPath, result, verbose) {
|
|
2684
|
+
const skillsDir = path9.join(rootPath, ".github", "skills");
|
|
2685
|
+
try {
|
|
2686
|
+
const entries = await fs8.readdir(skillsDir, { withFileTypes: true });
|
|
2687
|
+
const skillDirs = entries.filter((e) => e.isDirectory());
|
|
2688
|
+
if (skillDirs.length === 0) {
|
|
2689
|
+
result.warnings.push("No skills found in .github/skills/");
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
for (const dir of skillDirs) {
|
|
2693
|
+
const skillFile = path9.join(skillsDir, dir.name, "SKILL.md");
|
|
2694
|
+
try {
|
|
2695
|
+
const content = await fs8.readFile(skillFile, "utf-8");
|
|
2696
|
+
if (!content.startsWith("---")) {
|
|
2697
|
+
result.errors.push(`${dir.name}/SKILL.md: Missing YAML frontmatter`);
|
|
2698
|
+
result.valid = false;
|
|
2699
|
+
continue;
|
|
2700
|
+
}
|
|
2701
|
+
const frontmatterEnd = content.indexOf("---", 3);
|
|
2702
|
+
if (frontmatterEnd === -1) {
|
|
2703
|
+
result.errors.push(`${dir.name}/SKILL.md: Malformed YAML frontmatter`);
|
|
2704
|
+
result.valid = false;
|
|
2705
|
+
continue;
|
|
2706
|
+
}
|
|
2707
|
+
const frontmatter = content.slice(4, frontmatterEnd).trim();
|
|
2708
|
+
const meta = yaml2.parse(frontmatter);
|
|
2709
|
+
if (!meta.name) {
|
|
2710
|
+
result.errors.push(`${dir.name}/SKILL.md: Missing 'name' in frontmatter`);
|
|
2711
|
+
result.valid = false;
|
|
2712
|
+
}
|
|
2713
|
+
if (!meta.description) {
|
|
2714
|
+
result.warnings.push(`${dir.name}/SKILL.md: Missing 'description' in frontmatter`);
|
|
2715
|
+
}
|
|
2716
|
+
if (verbose) {
|
|
2717
|
+
console.log(chalk4.green(` \u2713 skills/${dir.name}/SKILL.md`));
|
|
2718
|
+
}
|
|
2719
|
+
} catch (e) {
|
|
2720
|
+
const err = e;
|
|
2721
|
+
result.errors.push(`${dir.name}: ${err.code === "ENOENT" ? "Missing SKILL.md file" : err.message}`);
|
|
2722
|
+
result.valid = false;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
if (!verbose) {
|
|
2726
|
+
console.log(chalk4.gray(` Validated ${skillDirs.length} skill(s)`));
|
|
2727
|
+
}
|
|
2728
|
+
} catch {
|
|
2729
|
+
result.warnings.push("No .github/skills/ directory found");
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
async function validateAgents(rootPath, result, verbose) {
|
|
2733
|
+
const agentsDir = path9.join(rootPath, ".github", "agents");
|
|
2734
|
+
try {
|
|
2735
|
+
const entries = await fs8.readdir(agentsDir, { withFileTypes: true });
|
|
2736
|
+
const agentMdFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".agent.md"));
|
|
2737
|
+
let validatedCount = 0;
|
|
2738
|
+
for (const file of agentMdFiles) {
|
|
2739
|
+
const agentFile = path9.join(agentsDir, file.name);
|
|
2740
|
+
try {
|
|
2741
|
+
const content = await fs8.readFile(agentFile, "utf-8");
|
|
2742
|
+
if (!content.startsWith("---")) {
|
|
2743
|
+
result.errors.push(`${file.name}: Missing YAML frontmatter`);
|
|
2744
|
+
result.valid = false;
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
2747
|
+
const frontmatterEnd = content.indexOf("---", 3);
|
|
2748
|
+
if (frontmatterEnd === -1) {
|
|
2749
|
+
result.errors.push(`${file.name}: Malformed YAML frontmatter`);
|
|
2750
|
+
result.valid = false;
|
|
2751
|
+
continue;
|
|
2752
|
+
}
|
|
2753
|
+
const frontmatter = content.slice(4, frontmatterEnd).trim();
|
|
2754
|
+
const meta = yaml2.parse(frontmatter);
|
|
2755
|
+
if (!meta.name) {
|
|
2756
|
+
result.errors.push(`${file.name}: Missing 'name' in frontmatter`);
|
|
2757
|
+
result.valid = false;
|
|
2758
|
+
}
|
|
2759
|
+
if (!meta.description) {
|
|
2760
|
+
result.warnings.push(`${file.name}: Missing 'description' in frontmatter`);
|
|
2761
|
+
}
|
|
2762
|
+
if (meta.tools && !Array.isArray(meta.tools)) {
|
|
2763
|
+
result.errors.push(`${file.name}: 'tools' must be an array`);
|
|
2764
|
+
result.valid = false;
|
|
2765
|
+
}
|
|
2766
|
+
validatedCount++;
|
|
2767
|
+
if (verbose) {
|
|
2768
|
+
console.log(chalk4.green(` \u2713 agents/${file.name}`));
|
|
2769
|
+
}
|
|
2770
|
+
} catch (e) {
|
|
2771
|
+
result.errors.push(`${file.name}: ${e.message}`);
|
|
2772
|
+
result.valid = false;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
if (validatedCount === 0) {
|
|
2776
|
+
result.errors.push("No .agent.md files found in .github/agents/");
|
|
2777
|
+
result.valid = false;
|
|
2778
|
+
}
|
|
2779
|
+
if (!verbose) {
|
|
2780
|
+
console.log(chalk4.gray(` Validated ${validatedCount} agent(s)`));
|
|
2781
|
+
}
|
|
2782
|
+
} catch {
|
|
2783
|
+
result.errors.push("No .github/agents/ directory found");
|
|
2784
|
+
result.valid = false;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
async function validateHooks(rootPath, result, verbose) {
|
|
2788
|
+
const hooksDir = path9.join(rootPath, ".github", "hooks");
|
|
2789
|
+
const validEvents = ["pre-commit", "post-commit", "pre-push", "pre-analyze", "post-generate"];
|
|
2790
|
+
try {
|
|
2791
|
+
const files = await fs8.readdir(hooksDir);
|
|
2792
|
+
const hookFiles = files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
2793
|
+
if (hookFiles.length === 0) {
|
|
2794
|
+
if (verbose) {
|
|
2795
|
+
console.log(chalk4.gray(" No hooks found"));
|
|
2796
|
+
}
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
for (const file of hookFiles) {
|
|
2800
|
+
const hookPath = path9.join(hooksDir, file);
|
|
2801
|
+
const content = await fs8.readFile(hookPath, "utf-8");
|
|
2802
|
+
try {
|
|
2803
|
+
const hook = yaml2.parse(content);
|
|
2804
|
+
if (!hook.name) {
|
|
2805
|
+
result.errors.push(`hooks/${file}: Missing 'name' field`);
|
|
2806
|
+
result.valid = false;
|
|
2807
|
+
}
|
|
2808
|
+
if (!hook.event) {
|
|
2809
|
+
result.errors.push(`hooks/${file}: Missing 'event' field`);
|
|
2810
|
+
result.valid = false;
|
|
2811
|
+
} else if (!validEvents.includes(hook.event)) {
|
|
2812
|
+
result.errors.push(
|
|
2813
|
+
`hooks/${file}: Invalid event '${hook.event}'. Must be one of: ${validEvents.join(", ")}`
|
|
2814
|
+
);
|
|
2815
|
+
result.valid = false;
|
|
2816
|
+
}
|
|
2817
|
+
if (!hook.commands || !Array.isArray(hook.commands) || hook.commands.length === 0) {
|
|
2818
|
+
result.errors.push(`hooks/${file}: Missing or empty 'commands' array`);
|
|
2819
|
+
result.valid = false;
|
|
2820
|
+
}
|
|
2821
|
+
if (verbose) {
|
|
2822
|
+
console.log(chalk4.green(` \u2713 hooks/${file}`));
|
|
2823
|
+
}
|
|
2824
|
+
} catch (e) {
|
|
2825
|
+
result.errors.push(`hooks/${file}: Invalid YAML - ${e.message}`);
|
|
2826
|
+
result.valid = false;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
if (!verbose) {
|
|
2830
|
+
console.log(chalk4.gray(` Validated ${hookFiles.length} hook(s)`));
|
|
2831
|
+
}
|
|
2832
|
+
} catch {
|
|
2833
|
+
if (verbose) {
|
|
2834
|
+
console.log(chalk4.gray(" No hooks directory"));
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
async function validateRegistry(rootPath, result, verbose) {
|
|
2839
|
+
const registryPath = path9.join(rootPath, "skills-registry.jsonl");
|
|
2840
|
+
try {
|
|
2841
|
+
const content = await fs8.readFile(registryPath, "utf-8");
|
|
2842
|
+
const lines = content.trim().split("\n").filter((l) => l.trim());
|
|
2843
|
+
if (lines.length === 0) {
|
|
2844
|
+
result.warnings.push("skills-registry.jsonl is empty");
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2848
|
+
try {
|
|
2849
|
+
const entry = JSON.parse(lines[i]);
|
|
2850
|
+
if (!entry.name || !entry.type) {
|
|
2851
|
+
result.errors.push(`skills-registry.jsonl line ${i + 1}: Missing 'name' or 'type'`);
|
|
2852
|
+
result.valid = false;
|
|
2853
|
+
}
|
|
2854
|
+
} catch {
|
|
2855
|
+
result.errors.push(`skills-registry.jsonl line ${i + 1}: Invalid JSON`);
|
|
2856
|
+
result.valid = false;
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
if (verbose) {
|
|
2860
|
+
console.log(chalk4.green(` \u2713 skills-registry.jsonl (${lines.length} entries)`));
|
|
2861
|
+
} else {
|
|
2862
|
+
console.log(chalk4.gray(` Validated registry (${lines.length} entries)`));
|
|
2863
|
+
}
|
|
2864
|
+
} catch {
|
|
2865
|
+
result.warnings.push("No skills-registry.jsonl found");
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
// src/main.ts
|
|
2870
|
+
var program = new Command();
|
|
2871
|
+
var banner = `
|
|
2872
|
+
${chalk5.green("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")}
|
|
2873
|
+
${chalk5.green("\u2551")} ${chalk5.bold.white("AGENT SMITH")} ${chalk5.green("\u2551")}
|
|
2874
|
+
${chalk5.green("\u2551")} ${chalk5.gray('"The best thing about being me...')} ${chalk5.green("\u2551")}
|
|
2875
|
+
${chalk5.green("\u2551")} ${chalk5.gray('there are so many of me."')} ${chalk5.green("\u2551")}
|
|
2876
|
+
${chalk5.green("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")}
|
|
2877
|
+
`;
|
|
2878
|
+
program.name("agentsmith").description("Assimilate any repository into a fully autonomous GitHub Copilot agent").version("0.3.0").addHelpText("beforeAll", banner).addHelpText("after", `
|
|
2879
|
+
${chalk5.bold("Examples:")}
|
|
2880
|
+
$ agentsmith assimilate . # Analyze current directory
|
|
2881
|
+
$ agentsmith assimilate https://github.com/expressjs/express # Analyze remote repo
|
|
2882
|
+
$ agentsmith assimilate . --dry-run --verbose # Preview with details
|
|
2883
|
+
$ agentsmith search "routing" # Search skills registry
|
|
2884
|
+
$ agentsmith validate # Validate generated assets
|
|
2885
|
+
|
|
2886
|
+
${chalk5.bold("Requirements:")}
|
|
2887
|
+
\u2022 Node.js 18+
|
|
2888
|
+
\u2022 GitHub Copilot subscription (for SDK access)
|
|
2889
|
+
\u2022 Copilot CLI installed and in PATH
|
|
2890
|
+
|
|
2891
|
+
${chalk5.bold("Documentation:")}
|
|
2892
|
+
https://github.com/shyamsridhar123/agentsmith-cli
|
|
2893
|
+
`);
|
|
2894
|
+
program.command("assimilate").description("Analyze a repository and generate agent assets (skills, agents, hooks)").argument("<target>", "Path to local repo or GitHub URL").option("-n, --dry-run", "Preview what would be generated without writing files").option("-v, --verbose", "Show detailed analysis output including file-by-file processing").option("-o, --output <path>", "Output directory for generated assets").option("--no-instructions", "Skip generation of .github/copilot-instructions.md").option("--single-agent", "Generate a single agent.md instead of multi-agent constellation (v0.3 compat)").addHelpText("after", `
|
|
2895
|
+
${chalk5.bold("Examples:")}
|
|
2896
|
+
$ agentsmith assimilate . # Local repository (multi-agent)
|
|
2897
|
+
$ agentsmith assimilate ~/projects/myapp # Specific path
|
|
2898
|
+
$ agentsmith assimilate https://github.com/expressjs/express # GitHub URL
|
|
2899
|
+
$ agentsmith assimilate . --dry-run # Preview mode
|
|
2900
|
+
$ agentsmith assimilate . -o ./output # Custom output
|
|
2901
|
+
$ agentsmith assimilate . --single-agent # Single agent (v0.3 compat)
|
|
2902
|
+
|
|
2903
|
+
${chalk5.bold("Generated assets:")}
|
|
2904
|
+
.github/copilot-instructions.md - Workspace-wide Copilot instructions
|
|
2905
|
+
.github/skills/<name>/SKILL.md - Reusable skill definitions
|
|
2906
|
+
.github/agents/<name>.agent.md - Agent configurations (multi-agent constellation)
|
|
2907
|
+
.github/copilot/handoffs.json - Agent delegation graph
|
|
2908
|
+
.github/hooks/*.yaml - Lifecycle hooks
|
|
2909
|
+
skills-registry.jsonl - Searchable index
|
|
2910
|
+
`).action(assimilateCommand);
|
|
2911
|
+
program.command("search").description("Search the skills and agents registry by keyword").argument("<query>", "Search query (matches name, description, triggers)").option("-l, --limit <number>", "Maximum results to return", "10").option("-t, --type <type>", "Filter by type: skill, agent, or hook").addHelpText("after", `
|
|
2912
|
+
${chalk5.bold("Examples:")}
|
|
2913
|
+
$ agentsmith search "routing" # Search all assets
|
|
2914
|
+
$ agentsmith search "test" --type skill # Only skills
|
|
2915
|
+
$ agentsmith search "api" --limit 5 # Limit results
|
|
2916
|
+
`).action(searchCommand);
|
|
2917
|
+
program.command("validate").description("Validate generated agent assets for correctness").argument("[path]", "Path to repository (default: current directory)", ".").option("-v, --verbose", "Show detailed validation output with all checks").addHelpText("after", `
|
|
2918
|
+
${chalk5.bold("Checks performed:")}
|
|
2919
|
+
\u2022 Skills have valid frontmatter (name, description)
|
|
2920
|
+
\u2022 Agents have required fields and valid skill references
|
|
2921
|
+
\u2022 Hooks have valid events and non-empty commands
|
|
2922
|
+
\u2022 Registry entries are valid JSON with required fields
|
|
2923
|
+
|
|
2924
|
+
${chalk5.bold("Examples:")}
|
|
2925
|
+
$ agentsmith validate # Current directory
|
|
2926
|
+
$ agentsmith validate ./my-project # Specific path
|
|
2927
|
+
$ agentsmith validate --verbose # Detailed output
|
|
2928
|
+
`).action(validateCommand);
|
|
2929
|
+
program.parse();
|
|
2930
|
+
//# sourceMappingURL=main.js.map
|