@crafter/skillkit 0.1.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/bin.ts +6 -1
- package/src/commands/health.ts +4 -2
- package/src/commands/list.ts +1 -9
- package/src/commands/scan.ts +30 -6
- package/src/db/queries.ts +37 -2
- package/src/index.ts +1 -1
- package/src/scanner/connectors/claude.ts +52 -7
- package/src/scanner/connectors/opencode.ts +4 -6
- package/src/scanner/index.ts +8 -5
- package/src/scanner/skills.ts +18 -20
package/package.json
CHANGED
package/src/bin.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
2
4
|
import { bold, cyan, dim, yellow } from "./tui/colors";
|
|
3
5
|
|
|
4
|
-
const
|
|
6
|
+
const pkg = JSON.parse(
|
|
7
|
+
readFileSync(join(dirname(import.meta.dir), "package.json"), "utf-8"),
|
|
8
|
+
);
|
|
9
|
+
const VERSION: string = pkg.version;
|
|
5
10
|
|
|
6
11
|
function printHelp(): void {
|
|
7
12
|
console.log(`
|
package/src/commands/health.ts
CHANGED
|
@@ -115,8 +115,10 @@ export async function runHealth(): Promise<void> {
|
|
|
115
115
|
console.log(`\n ${bold("SKILLKIT HEALTH REPORT")}\n`);
|
|
116
116
|
|
|
117
117
|
const agents = getDetectedAgents();
|
|
118
|
-
console.log(check(`${skills.length} skills
|
|
119
|
-
|
|
118
|
+
console.log(check(`${skills.length} skills installed`));
|
|
119
|
+
if (agents.length > 0) {
|
|
120
|
+
console.log(dim(` ${agents.join(" + ")}`));
|
|
121
|
+
}
|
|
120
122
|
|
|
121
123
|
if (dbExists && hasDbData) {
|
|
122
124
|
console.log(
|
package/src/commands/list.ts
CHANGED
|
@@ -20,11 +20,6 @@ export function runList(): void {
|
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const agentCounts = new Map<string, number>();
|
|
24
|
-
for (const s of skills) {
|
|
25
|
-
agentCounts.set(s.agent, (agentCounts.get(s.agent) ?? 0) + 1);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
23
|
const totalSize = skills.reduce((acc, s) => acc + s.size, 0);
|
|
29
24
|
|
|
30
25
|
console.log(`\n ${bold(`INSTALLED SKILLS (${skills.length})`)}\n`);
|
|
@@ -45,10 +40,7 @@ export function runList(): void {
|
|
|
45
40
|
console.log(` ${name}${desc}${size}`);
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
const agentSummary = [...agentCounts.entries()]
|
|
49
|
-
.map(([a, c]) => `${a} (${c})`)
|
|
50
|
-
.join(", ");
|
|
51
43
|
console.log(
|
|
52
|
-
`\n ${dim(`Total: ${skills.length} skills | ${formatSize(totalSize)}
|
|
44
|
+
`\n ${dim(`Total: ${skills.length} skills | ${formatSize(totalSize)}`)}\n`,
|
|
53
45
|
);
|
|
54
46
|
}
|
package/src/commands/scan.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { upsertInstalledSkill } from "../db/queries";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { upsertInstalledSkill, deduplicateInvocations } from "../db/queries";
|
|
4
4
|
import { getDb } from "../db/schema";
|
|
5
|
-
import {
|
|
5
|
+
import { countAllSessions, scanAllSessions } from "../scanner/index";
|
|
6
6
|
import { getDetectedAgents, scanInstalledSkills } from "../scanner/skills";
|
|
7
7
|
import { bold, cyan, dim } from "../tui/colors";
|
|
8
8
|
|
|
@@ -26,10 +26,12 @@ export async function runScan(): Promise<void> {
|
|
|
26
26
|
|
|
27
27
|
const agents = getDetectedAgents();
|
|
28
28
|
if (agents.length === 0) {
|
|
29
|
-
console.log(
|
|
29
|
+
console.log(
|
|
30
|
+
`\n ${dim("No supported agents found (Claude Code, OpenCode).")}\n`,
|
|
31
|
+
);
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
32
|
-
console.log(`\n ${dim(`Scanning ${agents.
|
|
34
|
+
console.log(`\n ${dim(`Scanning ${agents.join(" + ")}`)}`);
|
|
33
35
|
|
|
34
36
|
const skills = scanInstalledSkills();
|
|
35
37
|
|
|
@@ -84,10 +86,32 @@ export async function runScan(): Promise<void> {
|
|
|
84
86
|
} catch {}
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
const knownSkills = new Set<string>();
|
|
90
|
+
for (const skill of skills) {
|
|
91
|
+
knownSkills.add(skill.name);
|
|
92
|
+
knownSkills.add(basename(skill.path));
|
|
93
|
+
}
|
|
94
|
+
if (existsSync(localSkillsDir)) {
|
|
95
|
+
try {
|
|
96
|
+
for (const e of readdirSync(localSkillsDir)) {
|
|
97
|
+
try {
|
|
98
|
+
if (statSync(join(localSkillsDir, e)).isDirectory()) {
|
|
99
|
+
knownSkills.add(e);
|
|
100
|
+
}
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const removed = deduplicateInvocations(db);
|
|
107
|
+
if (removed > 0) {
|
|
108
|
+
console.log(` ${dim(`Cleaned ${removed} duplicate entries`)}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
87
111
|
console.log(dim(" Scanning sessions..."));
|
|
88
112
|
|
|
89
113
|
const sessionCount = countAllSessions();
|
|
90
|
-
const newInvocations = await scanAllSessions(db);
|
|
114
|
+
const newInvocations = await scanAllSessions(db, knownSkills);
|
|
91
115
|
const totalRow = db
|
|
92
116
|
.query<{ count: number }, []>(
|
|
93
117
|
"SELECT COUNT(*) as count FROM skill_invocations",
|
package/src/db/queries.ts
CHANGED
|
@@ -76,18 +76,53 @@ export function recordInvocation(
|
|
|
76
76
|
skillName: string,
|
|
77
77
|
sessionId?: string,
|
|
78
78
|
project?: string,
|
|
79
|
+
timestamp?: string,
|
|
79
80
|
): void {
|
|
81
|
+
const ts = timestamp ?? new Date().toISOString();
|
|
80
82
|
db.run(
|
|
81
83
|
"INSERT INTO skill_invocations (skill_name, timestamp, session_id, project) VALUES (?, ?, ?, ?)",
|
|
82
|
-
[skillName,
|
|
84
|
+
[skillName, ts, sessionId ?? null, project ?? null],
|
|
83
85
|
);
|
|
84
|
-
const date =
|
|
86
|
+
const date = ts.slice(0, 10);
|
|
85
87
|
db.run(
|
|
86
88
|
"INSERT INTO skill_daily_stats (date, skill_name, count) VALUES (?, ?, 1) ON CONFLICT(date, skill_name) DO UPDATE SET count = count + 1",
|
|
87
89
|
[date, skillName],
|
|
88
90
|
);
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
export function deduplicateInvocations(db: Database): number {
|
|
94
|
+
const before = db
|
|
95
|
+
.query<{ count: number }, []>(
|
|
96
|
+
"SELECT COUNT(*) as count FROM skill_invocations",
|
|
97
|
+
)
|
|
98
|
+
.get()?.count ?? 0;
|
|
99
|
+
|
|
100
|
+
db.run(`
|
|
101
|
+
DELETE FROM skill_invocations WHERE id NOT IN (
|
|
102
|
+
SELECT MIN(id) FROM skill_invocations
|
|
103
|
+
GROUP BY skill_name, session_id, timestamp
|
|
104
|
+
)
|
|
105
|
+
`);
|
|
106
|
+
|
|
107
|
+
const after = db
|
|
108
|
+
.query<{ count: number }, []>(
|
|
109
|
+
"SELECT COUNT(*) as count FROM skill_invocations",
|
|
110
|
+
)
|
|
111
|
+
.get()?.count ?? 0;
|
|
112
|
+
|
|
113
|
+
if (before !== after) {
|
|
114
|
+
db.run("DELETE FROM skill_daily_stats");
|
|
115
|
+
db.run(`
|
|
116
|
+
INSERT INTO skill_daily_stats (date, skill_name, count)
|
|
117
|
+
SELECT date(timestamp), skill_name, COUNT(*)
|
|
118
|
+
FROM skill_invocations
|
|
119
|
+
GROUP BY date(timestamp), skill_name
|
|
120
|
+
`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return before - after;
|
|
124
|
+
}
|
|
125
|
+
|
|
91
126
|
export function upsertInstalledSkill(
|
|
92
127
|
db: Database,
|
|
93
128
|
name: string,
|
package/src/index.ts
CHANGED
|
@@ -12,8 +12,8 @@ export {
|
|
|
12
12
|
upsertInstalledSkill,
|
|
13
13
|
} from "./db/queries";
|
|
14
14
|
export { getDb } from "./db/schema";
|
|
15
|
-
export { scanAllSessions, countAllSessions } from "./scanner/index";
|
|
16
15
|
export { parseSessionFile } from "./scanner/connectors/claude";
|
|
16
|
+
export { countAllSessions, scanAllSessions } from "./scanner/index";
|
|
17
17
|
export { getDetectedAgents, scanInstalledSkills } from "./scanner/skills";
|
|
18
18
|
export type {
|
|
19
19
|
InstalledSkill,
|
|
@@ -19,7 +19,28 @@ function extractSkillName(block: ToolUseBlock): string | null {
|
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const COMMAND_NAME_RE = /<command-name>\/?([a-zA-Z][\w-]*(?::[\w-]*)*)<\/command-name>/g;
|
|
23
|
+
|
|
24
|
+
function extractCommandNames(
|
|
25
|
+
text: string,
|
|
26
|
+
knownSkills: Set<string>,
|
|
27
|
+
): string[] {
|
|
28
|
+
const names: string[] = [];
|
|
29
|
+
let match: RegExpExecArray | null;
|
|
30
|
+
while ((match = COMMAND_NAME_RE.exec(text)) !== null) {
|
|
31
|
+
const name = match[1];
|
|
32
|
+
if (knownSkills.has(name)) {
|
|
33
|
+
names.push(name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
COMMAND_NAME_RE.lastIndex = 0;
|
|
37
|
+
return names;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseSessionFile(
|
|
41
|
+
filePath: string,
|
|
42
|
+
knownSkills: Set<string> = new Set(),
|
|
43
|
+
): Invocation[] {
|
|
23
44
|
const results: Invocation[] = [];
|
|
24
45
|
const sessionId = basename(filePath, ".jsonl");
|
|
25
46
|
|
|
@@ -49,10 +70,35 @@ export function parseSessionFile(filePath: string): Invocation[] {
|
|
|
49
70
|
: new Date().toISOString();
|
|
50
71
|
|
|
51
72
|
const msg = obj.message as
|
|
52
|
-
| { content: Array<Record<string, unknown>> }
|
|
73
|
+
| { content: Array<Record<string, unknown>> | string }
|
|
53
74
|
| undefined;
|
|
75
|
+
|
|
76
|
+
if (obj.type === "user" && msg) {
|
|
77
|
+
const text =
|
|
78
|
+
typeof msg.content === "string"
|
|
79
|
+
? msg.content
|
|
80
|
+
: Array.isArray(msg.content)
|
|
81
|
+
? msg.content
|
|
82
|
+
.filter(
|
|
83
|
+
(b): b is { type: string; text: string } =>
|
|
84
|
+
typeof b === "object" &&
|
|
85
|
+
b !== null &&
|
|
86
|
+
b.type === "text" &&
|
|
87
|
+
typeof b.text === "string",
|
|
88
|
+
)
|
|
89
|
+
.map((b) => b.text)
|
|
90
|
+
.join("\n")
|
|
91
|
+
: "";
|
|
92
|
+
for (const name of extractCommandNames(text, knownSkills)) {
|
|
93
|
+
results.push({ skillName: name, timestamp, sessionId });
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
54
98
|
const msgContent =
|
|
55
|
-
obj.type === "assistant" && msg
|
|
99
|
+
obj.type === "assistant" && msg && Array.isArray(msg.content)
|
|
100
|
+
? (msg.content as Array<Record<string, unknown>>)
|
|
101
|
+
: null;
|
|
56
102
|
|
|
57
103
|
if (!Array.isArray(msgContent)) continue;
|
|
58
104
|
|
|
@@ -63,9 +109,7 @@ export function parseSessionFile(filePath: string): Invocation[] {
|
|
|
63
109
|
block.type === "tool_use" &&
|
|
64
110
|
(block as unknown as ToolUseBlock).name === "Skill"
|
|
65
111
|
) {
|
|
66
|
-
const skillName = extractSkillName(
|
|
67
|
-
block as unknown as ToolUseBlock,
|
|
68
|
-
);
|
|
112
|
+
const skillName = extractSkillName(block as unknown as ToolUseBlock);
|
|
69
113
|
if (skillName) {
|
|
70
114
|
results.push({ skillName, timestamp, sessionId });
|
|
71
115
|
}
|
|
@@ -91,6 +135,7 @@ export function countClaudeSessions(): number {
|
|
|
91
135
|
export async function scanClaudeSessions(
|
|
92
136
|
db: Database,
|
|
93
137
|
trackedSet: Set<string>,
|
|
138
|
+
knownSkills: Set<string> = new Set(),
|
|
94
139
|
): Promise<number> {
|
|
95
140
|
const projectsDir = join(homedir(), ".claude", "projects");
|
|
96
141
|
if (!existsSync(projectsDir)) return 0;
|
|
@@ -104,7 +149,7 @@ export async function scanClaudeSessions(
|
|
|
104
149
|
|
|
105
150
|
let total = 0;
|
|
106
151
|
for (const file of files) {
|
|
107
|
-
const invocations = parseSessionFile(file);
|
|
152
|
+
const invocations = parseSessionFile(file, knownSkills);
|
|
108
153
|
total += recordNewInvocations(db, trackedSet, invocations);
|
|
109
154
|
}
|
|
110
155
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Database as BunDatabase } from "bun:sqlite";
|
|
2
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { recordNewInvocations } from "../index";
|
|
7
6
|
import type { Invocation } from "../index";
|
|
7
|
+
import { recordNewInvocations } from "../index";
|
|
8
8
|
|
|
9
9
|
function getDbPath(): string | null {
|
|
10
10
|
const os = platform();
|
|
@@ -48,9 +48,7 @@ export function countOpenCodeSessions(): number {
|
|
|
48
48
|
|
|
49
49
|
try {
|
|
50
50
|
const row = ocDb
|
|
51
|
-
.query<{ count: number }, []>(
|
|
52
|
-
"SELECT COUNT(*) as count FROM session",
|
|
53
|
-
)
|
|
51
|
+
.query<{ count: number }, []>("SELECT COUNT(*) as count FROM session")
|
|
54
52
|
.get();
|
|
55
53
|
return row?.count ?? 0;
|
|
56
54
|
} catch {
|
|
@@ -70,7 +68,7 @@ export function scanOpenCodeSessions(
|
|
|
70
68
|
try {
|
|
71
69
|
const rows = ocDb
|
|
72
70
|
.query<PartRow, []>(
|
|
73
|
-
|
|
71
|
+
'SELECT p.session_id, p.time_created, p.data FROM part p WHERE p.data LIKE \'%"tool":"skill"%\'',
|
|
74
72
|
)
|
|
75
73
|
.all();
|
|
76
74
|
|
package/src/scanner/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
2
|
import { recordInvocation } from "../db/queries";
|
|
3
|
-
import {
|
|
3
|
+
import { countClaudeSessions, scanClaudeSessions } from "./connectors/claude";
|
|
4
4
|
import {
|
|
5
|
-
scanOpenCodeSessions,
|
|
6
5
|
countOpenCodeSessions,
|
|
6
|
+
scanOpenCodeSessions,
|
|
7
7
|
} from "./connectors/opencode";
|
|
8
8
|
|
|
9
9
|
interface AlreadyTracked {
|
|
@@ -35,7 +35,7 @@ export function recordNewInvocations(
|
|
|
35
35
|
for (const inv of invocations) {
|
|
36
36
|
const key = `${inv.sessionId}::${inv.timestamp}`;
|
|
37
37
|
if (!trackedSet.has(key)) {
|
|
38
|
-
recordInvocation(db, inv.skillName, inv.sessionId);
|
|
38
|
+
recordInvocation(db, inv.skillName, inv.sessionId, undefined, inv.timestamp);
|
|
39
39
|
trackedSet.add(key);
|
|
40
40
|
count++;
|
|
41
41
|
}
|
|
@@ -43,10 +43,13 @@ export function recordNewInvocations(
|
|
|
43
43
|
return count;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export async function scanAllSessions(
|
|
46
|
+
export async function scanAllSessions(
|
|
47
|
+
db: Database,
|
|
48
|
+
knownSkills: Set<string> = new Set(),
|
|
49
|
+
): Promise<number> {
|
|
47
50
|
const trackedSet = getTrackedSet(db);
|
|
48
51
|
let total = 0;
|
|
49
|
-
total += await scanClaudeSessions(db, trackedSet);
|
|
52
|
+
total += await scanClaudeSessions(db, trackedSet, knownSkills);
|
|
50
53
|
total += scanOpenCodeSessions(db, trackedSet);
|
|
51
54
|
return total;
|
|
52
55
|
}
|
package/src/scanner/skills.ts
CHANGED
|
@@ -3,22 +3,23 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { InstalledSkill } from "../types";
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const SUPPORTED_AGENTS: Array<{ agent: string; dir: string }> = [
|
|
7
7
|
{ agent: "Claude Code", dir: join(homedir(), ".claude", "skills") },
|
|
8
|
-
{ agent: "Cursor", dir: join(homedir(), ".cursor", "skills") },
|
|
9
|
-
{ agent: "Codex", dir: join(homedir(), ".codex", "skills") },
|
|
10
|
-
{ agent: "Windsurf", dir: join(homedir(), ".codeium", "windsurf", "skills") },
|
|
11
|
-
{ agent: "Gemini CLI", dir: join(homedir(), ".gemini", "skills") },
|
|
12
|
-
{ agent: "Cline", dir: join(homedir(), ".cline", "skills") },
|
|
13
|
-
{ agent: "Roo Code", dir: join(homedir(), ".roo", "skills") },
|
|
14
|
-
{ agent: "Continue", dir: join(homedir(), ".continue", "skills") },
|
|
15
8
|
{ agent: "OpenCode", dir: join(homedir(), ".config", "opencode", "skills") },
|
|
16
|
-
|
|
17
|
-
{ agent: "
|
|
18
|
-
{ agent: "
|
|
19
|
-
{ agent: "
|
|
20
|
-
{ agent: "
|
|
21
|
-
{ agent: "
|
|
9
|
+
// Planned — needs session connector to enable full analytics pipeline
|
|
10
|
+
// { agent: "Cursor", dir: join(homedir(), ".cursor", "skills") }, // GH-1: injects skills as context rules, no discrete tool_use
|
|
11
|
+
// { agent: "Codex", dir: join(homedir(), ".codex", "skills") }, // GH-2
|
|
12
|
+
// { agent: "Windsurf", dir: join(homedir(), ".codeium", "windsurf", "skills") }, // GH-3
|
|
13
|
+
// { agent: "Gemini CLI", dir: join(homedir(), ".gemini", "skills") }, // GH-4
|
|
14
|
+
// { agent: "Cline", dir: join(homedir(), ".cline", "skills") }, // GH-5
|
|
15
|
+
// { agent: "Roo Code", dir: join(homedir(), ".roo", "skills") }, // GH-6
|
|
16
|
+
// { agent: "Continue", dir: join(homedir(), ".continue", "skills") }, // GH-7
|
|
17
|
+
// { agent: "GitHub Copilot", dir: join(homedir(), ".copilot", "skills") }, // GH-8
|
|
18
|
+
// { agent: "OpenHands", dir: join(homedir(), ".openhands", "skills") }, // GH-9
|
|
19
|
+
// { agent: "Amp", dir: join(homedir(), ".config", "agents", "skills") }, // GH-10
|
|
20
|
+
// { agent: "Goose", dir: join(homedir(), ".config", "goose", "skills") }, // GH-11
|
|
21
|
+
// { agent: "Kilo Code", dir: join(homedir(), ".kilocode", "skills") }, // GH-12
|
|
22
|
+
// { agent: "Trae", dir: join(homedir(), ".trae", "skills") }, // GH-13
|
|
22
23
|
];
|
|
23
24
|
|
|
24
25
|
function parseYamlFrontmatter(content: string): Record<string, string> {
|
|
@@ -56,10 +57,7 @@ function getDirSize(dirPath: string): number {
|
|
|
56
57
|
return total;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
function scanSkillsDir(
|
|
60
|
-
skillsDir: string,
|
|
61
|
-
agent: string,
|
|
62
|
-
): InstalledSkill[] {
|
|
60
|
+
function scanSkillsDir(skillsDir: string, agent: string): InstalledSkill[] {
|
|
63
61
|
if (!existsSync(skillsDir)) return [];
|
|
64
62
|
|
|
65
63
|
const skills: InstalledSkill[] = [];
|
|
@@ -125,7 +123,7 @@ export function scanInstalledSkills(): InstalledSkill[] {
|
|
|
125
123
|
const allSkills: InstalledSkill[] = [];
|
|
126
124
|
const seen = new Set<string>();
|
|
127
125
|
|
|
128
|
-
for (const { agent, dir } of
|
|
126
|
+
for (const { agent, dir } of SUPPORTED_AGENTS) {
|
|
129
127
|
const skills = scanSkillsDir(dir, agent);
|
|
130
128
|
for (const skill of skills) {
|
|
131
129
|
try {
|
|
@@ -143,7 +141,7 @@ export function scanInstalledSkills(): InstalledSkill[] {
|
|
|
143
141
|
|
|
144
142
|
export function getDetectedAgents(): string[] {
|
|
145
143
|
const agents: string[] = [];
|
|
146
|
-
for (const { agent, dir } of
|
|
144
|
+
for (const { agent, dir } of SUPPORTED_AGENTS) {
|
|
147
145
|
if (existsSync(dir)) agents.push(agent);
|
|
148
146
|
}
|
|
149
147
|
return agents;
|