prehello 0.1.0 → 1.0.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/CHANGELOG.md +46 -8
- package/README.md +73 -417
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/adapters/common.js +3 -3
- package/dist/adapters/cursor.js +20 -2
- package/dist/adapters/gemini-cli.js +214 -0
- package/dist/adapters/github-copilot.js +249 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/skills.js +10 -0
- package/dist/agents.js +54 -0
- package/dist/calibrate.js +265 -0
- package/dist/cli.js +144 -7
- package/dist/context.js +4 -6
- package/dist/diff.js +182 -0
- package/dist/mcp/configs.js +1 -1
- package/dist/model.js +3 -6
- package/dist/render/card.js +5 -4
- package/dist/render/markdown.js +1 -1
- package/dist/render/sarif.js +2 -2
- package/dist/render/terminal.js +8 -19
- package/dist/safe-write.js +2 -0
- package/dist/scan.js +5 -11
- package/dist/text/imports.js +7 -2
- package/dist/tokens/families.js +48 -16
- package/dist/version.js +5 -0
- package/package.json +8 -4
- package/spec/calibration.json +33 -0
- package/spec/cursor.json +1 -1
- package/spec/gemini-cli.json +17 -0
- package/spec/github-copilot.json +14 -0
package/dist/adapters/cursor.js
CHANGED
|
@@ -3,8 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CURSOR_LISTING_NOTE = exports.CURSOR_DESC_CHARS = void 0;
|
|
6
7
|
exports.detectCursor = detectCursor;
|
|
7
8
|
exports.discoverCursor = discoverCursor;
|
|
9
|
+
exports.cursorSkillLine = cursorSkillLine;
|
|
8
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
11
|
const spec_1 = require("../spec");
|
|
10
12
|
const fs_safe_1 = require("../fs-safe");
|
|
@@ -180,14 +182,19 @@ function discoverCursor(ctx) {
|
|
|
180
182
|
}
|
|
181
183
|
// Skills. Listing (name+description) is assumed to load up front, bodies on demand; listing format/cost is UNVERIFIED for Cursor.
|
|
182
184
|
const seenSkill = new Set();
|
|
183
|
-
const cursorSkillNotes = (extra) => [
|
|
185
|
+
const cursorSkillNotes = (extra) => [exports.CURSOR_LISTING_NOTE, ...(extra ? [extra] : [])];
|
|
184
186
|
const emitSkills = (recs, dir, scope, tag, confidence, note, forceConditional = false) => {
|
|
185
187
|
const fresh = recs.filter((r) => !seenSkill.has(r.realPath));
|
|
186
188
|
for (const r of fresh)
|
|
187
189
|
seenSkill.add(r.realPath);
|
|
188
190
|
const unscoped = fresh.filter((r) => r.scopedTo.length === 0);
|
|
189
191
|
const scoped = fresh.filter((r) => r.scopedTo.length > 0);
|
|
190
|
-
const spec = {
|
|
192
|
+
const spec = {
|
|
193
|
+
agent: AGENT, dir, scope, confidence, evidence: ev(SKILLS_DOC),
|
|
194
|
+
lineFor: (r) => (r.disableModelInvocation ? null : cursorSkillLine(dir, r, "short")),
|
|
195
|
+
lowLineFor: (r) => cursorSkillLine(dir, r, "none"),
|
|
196
|
+
highLineFor: (r) => cursorSkillLine(dir, r, "full"),
|
|
197
|
+
};
|
|
191
198
|
if (unscoped.length)
|
|
192
199
|
items.push(...(0, skills_1.skillItems)(ctx, { ...spec, tag, recs: unscoped, listingMode: forceConditional ? "conditional" : "always", notes: cursorSkillNotes(note) }));
|
|
193
200
|
if (scoped.length)
|
|
@@ -255,3 +262,14 @@ function discoverCursor(ctx) {
|
|
|
255
262
|
}
|
|
256
263
|
return { items, unmeasuredBanner: banner };
|
|
257
264
|
}
|
|
265
|
+
/** Observed Cursor listing (forum.cursor.com/t/163761, staff-confirmed 2026-06-22): description cut to about 80 characters. */
|
|
266
|
+
exports.CURSOR_DESC_CHARS = 80;
|
|
267
|
+
const CURSOR_PATH_STANDIN = "/Users/you/.cursor/skills";
|
|
268
|
+
exports.CURSOR_LISTING_NOTE = "Cursor lists each skill as <agent_skill fullPath=...> with its description cut to ~80 characters, and drops descriptions when the catalog is large (observed, confirmed by Cursor staff 2026-06; budget undocumented). Estimate = 80-character form; low = path only; high = full description.";
|
|
269
|
+
/** One `<agent_skill>` entry. The absolute path is approximated by a fixed stand-in + the path below the skills root (deterministic, never output). */
|
|
270
|
+
function cursorSkillLine(root, r, desc) {
|
|
271
|
+
const rel = node_path_1.default.relative(root, r.file).split(node_path_1.default.sep).join("/");
|
|
272
|
+
const d = r.desc.trim();
|
|
273
|
+
const text = desc === "none" ? "" : desc === "full" || d.length <= exports.CURSOR_DESC_CHARS ? d : `${d.slice(0, exports.CURSOR_DESC_CHARS)}...`;
|
|
274
|
+
return `<agent_skill fullPath="${CURSOR_PATH_STANDIN}/${rel}">${text}</agent_skill>`;
|
|
275
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.detectGemini = detectGemini;
|
|
7
|
+
exports.discoverGemini = discoverGemini;
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const context_1 = require("../context");
|
|
10
|
+
const spec_1 = require("../spec");
|
|
11
|
+
const fs_safe_1 = require("../fs-safe");
|
|
12
|
+
const common_1 = require("./common");
|
|
13
|
+
const configs_1 = require("../mcp/configs");
|
|
14
|
+
const skills_1 = require("./skills");
|
|
15
|
+
const AGENT = "gemini-cli";
|
|
16
|
+
const ev = (doc) => (0, spec_1.evidenceFor)(AGENT, doc);
|
|
17
|
+
function geminiHome(ctx) {
|
|
18
|
+
return node_path_1.default.join(ctx.home, ".gemini");
|
|
19
|
+
}
|
|
20
|
+
function detectGemini(ctx) {
|
|
21
|
+
const by = [];
|
|
22
|
+
if ((0, fs_safe_1.isDir)(geminiHome(ctx)))
|
|
23
|
+
by.push("config dir");
|
|
24
|
+
if ((0, fs_safe_1.onPath)("gemini", ctx.opts.env.PATH ?? ctx.opts.env.Path, { platform: ctx.opts.platform, pathExt: ctx.opts.env.PATHEXT }))
|
|
25
|
+
by.push("binary on PATH");
|
|
26
|
+
const repo = [];
|
|
27
|
+
for (const rel of ["GEMINI.md", ".gemini"]) {
|
|
28
|
+
const p = node_path_1.default.join(ctx.cwd, rel);
|
|
29
|
+
if ((0, fs_safe_1.isFile)(p) || (0, fs_safe_1.isDir)(p))
|
|
30
|
+
repo.push(p);
|
|
31
|
+
}
|
|
32
|
+
return { detectedBy: by, repoFiles: repo };
|
|
33
|
+
}
|
|
34
|
+
const isObj = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
|
|
35
|
+
/** settings.json may carry // comments in the wild; only strict JSON is parsed here (a failure is a warning, never a guess). */
|
|
36
|
+
function readSettings(ctx, file, jail) {
|
|
37
|
+
if (!(0, fs_safe_1.isFile)(file))
|
|
38
|
+
return undefined;
|
|
39
|
+
const r = ctx.read(file, jail ? { jail } : {});
|
|
40
|
+
if (!r.ok)
|
|
41
|
+
return undefined;
|
|
42
|
+
try {
|
|
43
|
+
const j = JSON.parse(r.text);
|
|
44
|
+
return isObj(j) ? j : undefined;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
ctx.warn(`${ctx.display(file)}: could not parse as JSON, skipped`);
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function names(v) {
|
|
52
|
+
const list = typeof v === "string" ? [v] : Array.isArray(v) ? v : undefined;
|
|
53
|
+
if (!list)
|
|
54
|
+
return undefined;
|
|
55
|
+
const ok = list.filter((x) => typeof x === "string" && x.length > 0 && !/[\\/]/.test(x) && x !== "." && x !== "..");
|
|
56
|
+
return ok.length ? [...new Set(ok)] : undefined;
|
|
57
|
+
}
|
|
58
|
+
function discoverGemini(ctx) {
|
|
59
|
+
const caps = (0, spec_1.loadSpec)(AGENT).caps;
|
|
60
|
+
const items = [];
|
|
61
|
+
const banner = ["built-in system prompt and tools", "the directory tree Gemini CLI adds to the first request (context.includeDirectoryTree)", "saved memories and auto memory"];
|
|
62
|
+
const home = geminiHome(ctx);
|
|
63
|
+
const userSettingsFile = node_path_1.default.join(home, "settings.json");
|
|
64
|
+
const wsSettingsFile = node_path_1.default.join(ctx.cwd, ".gemini", "settings.json");
|
|
65
|
+
const user = readSettings(ctx, userSettingsFile) ?? {};
|
|
66
|
+
const ws = ctx.cwd === ctx.home ? {} : (readSettings(ctx, wsSettingsFile, ctx.jailFor(ctx.cwd)) ?? {});
|
|
67
|
+
const ctxOf = (s) => (isObj(s.context) ? s.context : {});
|
|
68
|
+
// Workspace settings override user settings (documented).
|
|
69
|
+
const pick = (k) => (ctxOf(ws)[k] !== undefined ? ctxOf(ws)[k] : ctxOf(user)[k]);
|
|
70
|
+
const fileNames = names(pick("fileName")) ?? names(ws.contextFileName) ?? names(user.contextFileName) ?? ["GEMINI.md"];
|
|
71
|
+
const markersRaw = pick("memoryBoundaryMarkers");
|
|
72
|
+
const markers = Array.isArray(markersRaw) ? markersRaw.filter((x) => typeof x === "string" && x.length > 0) : [".git"];
|
|
73
|
+
const customNames = !(fileNames.length === 1 && fileNames[0] === "GEMINI.md");
|
|
74
|
+
const nameNote = customNames ? [`context.fileName: ${fileNames.join(", ")}`] : [];
|
|
75
|
+
const importOpts = { imports: true, importMaxDepth: caps.importMaxDepth, stripComments: false };
|
|
76
|
+
// 1. Global context files
|
|
77
|
+
for (const n of fileNames) {
|
|
78
|
+
const f = node_path_1.default.join(home, n);
|
|
79
|
+
const it = (0, common_1.loadInstruction)(ctx, f, { agent: AGENT, scope: "user", loadMode: "always", label: `~/.gemini/${n}`, cardLabel: `global ${n}`, ...importOpts, confidence: "documented", evidence: ev("geminiMd"), notes: nameNote });
|
|
80
|
+
if (it)
|
|
81
|
+
items.push(it);
|
|
82
|
+
}
|
|
83
|
+
// 2. cwd and parents, up to the first directory holding a boundary marker (default .git), never the home folder itself
|
|
84
|
+
const up = [];
|
|
85
|
+
for (let d = ctx.cwd;;) {
|
|
86
|
+
if (d === ctx.home)
|
|
87
|
+
break;
|
|
88
|
+
up.push(d);
|
|
89
|
+
if (!markers.length)
|
|
90
|
+
break; // an empty list disables parent traversal (documented)
|
|
91
|
+
if (markers.some((m) => (0, fs_safe_1.isFile)(node_path_1.default.join(d, m)) || (0, fs_safe_1.isDir)(node_path_1.default.join(d, m))))
|
|
92
|
+
break;
|
|
93
|
+
if (ctx.opts.ceiling && d === ctx.opts.ceiling)
|
|
94
|
+
break;
|
|
95
|
+
const p = node_path_1.default.dirname(d);
|
|
96
|
+
if (p === d)
|
|
97
|
+
break;
|
|
98
|
+
d = p;
|
|
99
|
+
}
|
|
100
|
+
// Files above the boundary are not part of this project's context: say so instead of dropping them silently.
|
|
101
|
+
const top = up[up.length - 1];
|
|
102
|
+
const exists = (p) => (0, fs_safe_1.isFile)(p) || (0, fs_safe_1.isDir)(p);
|
|
103
|
+
if (top && markers.some((m) => exists(node_path_1.default.join(top, m)))) {
|
|
104
|
+
for (let d = top;;) {
|
|
105
|
+
if (ctx.opts.ceiling && d === ctx.opts.ceiling)
|
|
106
|
+
break;
|
|
107
|
+
const p = node_path_1.default.dirname(d);
|
|
108
|
+
if (p === d || p === ctx.home)
|
|
109
|
+
break;
|
|
110
|
+
d = p;
|
|
111
|
+
for (const n of fileNames)
|
|
112
|
+
if ((0, fs_safe_1.isFile)(node_path_1.default.join(d, n)))
|
|
113
|
+
ctx.exclude(AGENT, node_path_1.default.join(d, n), `above the project root (${markers.join(", ")} boundary): Gemini CLI stops there`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const dir of up.reverse()) {
|
|
117
|
+
for (const n of fileNames) {
|
|
118
|
+
const f = node_path_1.default.join(dir, n);
|
|
119
|
+
if (!(0, fs_safe_1.isFile)(f))
|
|
120
|
+
continue;
|
|
121
|
+
const it = (0, common_1.loadInstruction)(ctx, f, {
|
|
122
|
+
agent: AGENT, scope: dir === ctx.cwd ? "project" : "ancestor", loadMode: "always", label: ctx.display(f), cardLabel: n, jail: ctx.jailFor(dir),
|
|
123
|
+
...importOpts, confidence: "documented", evidence: ev("geminiMd"), notes: nameNote,
|
|
124
|
+
});
|
|
125
|
+
if (it)
|
|
126
|
+
items.push(it);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 3. Below cwd: just-in-time (loaded when a tool touches that directory). The project walk knows a fixed set of names.
|
|
130
|
+
const walkable = fileNames.filter((n) => context_1.WALK_CANDIDATES.includes(n));
|
|
131
|
+
let nNested = 0;
|
|
132
|
+
for (const hit of walkable.length ? ctx.walk().hits : []) {
|
|
133
|
+
if (hit.dir === ctx.cwd || !(0, fs_safe_1.isInside)(hit.dir, ctx.cwd))
|
|
134
|
+
continue;
|
|
135
|
+
for (const n of walkable) {
|
|
136
|
+
if (!hit.names.includes(n))
|
|
137
|
+
continue;
|
|
138
|
+
if (++nNested > caps.discoveryMaxDirs)
|
|
139
|
+
break;
|
|
140
|
+
const f = node_path_1.default.join(hit.dir, n);
|
|
141
|
+
const it = (0, common_1.loadInstruction)(ctx, f, {
|
|
142
|
+
agent: AGENT, scope: "nested", loadMode: "conditional", label: ctx.display(f), cardLabel: `nested ${n}`, jail: ctx.jailFor(hit.dir), ...importOpts,
|
|
143
|
+
confidence: "documented", evidence: ev("geminiMd"),
|
|
144
|
+
notes: ["just-in-time: loaded when a tool accesses that directory (docs/cli/gemini-md.md); older docs describe an eager scan of up to 200 subdirectories, which would make it always-on"],
|
|
145
|
+
});
|
|
146
|
+
if (it)
|
|
147
|
+
items.push(it);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const unwalked = fileNames.filter((n) => !walkable.includes(n));
|
|
151
|
+
if (unwalked.length)
|
|
152
|
+
banner.push(`context files named ${unwalked.join(", ")} below the working directory (not walked)`);
|
|
153
|
+
// 4. Extensions (~/.gemini/extensions/<name>/gemini-extension.json): context file + MCP servers
|
|
154
|
+
const extServers = [];
|
|
155
|
+
const extRoot = node_path_1.default.join(home, "extensions");
|
|
156
|
+
const exts = (0, fs_safe_1.listDir)(extRoot).filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
157
|
+
for (const e of exts) {
|
|
158
|
+
const dir = node_path_1.default.join(extRoot, e.name);
|
|
159
|
+
const manifestFile = node_path_1.default.join(dir, "gemini-extension.json");
|
|
160
|
+
const m = readSettings(ctx, manifestFile, dir);
|
|
161
|
+
if (!m)
|
|
162
|
+
continue;
|
|
163
|
+
const note = "installed extension; per-workspace enable/disable state is not read (assumed enabled)";
|
|
164
|
+
const ctxNames = names(m.contextFileName) ?? ((0, fs_safe_1.isFile)(node_path_1.default.join(dir, "GEMINI.md")) ? ["GEMINI.md"] : []);
|
|
165
|
+
for (const n of ctxNames) {
|
|
166
|
+
const f = node_path_1.default.join(dir, n);
|
|
167
|
+
const it = (0, common_1.loadInstruction)(ctx, f, { agent: AGENT, scope: "plugin", loadMode: "always", label: `extension ${e.name}: ${n}`, cardLabel: "extension context", jail: dir, ...importOpts, confidence: "documented", evidence: ev("extensions"), notes: [note] });
|
|
168
|
+
if (it)
|
|
169
|
+
items.push(it);
|
|
170
|
+
}
|
|
171
|
+
for (const def of (0, configs_1.parseMcpMap)(m.mcpServers, (w) => ctx.warn(w)))
|
|
172
|
+
extServers.push({ file: manifestFile, def });
|
|
173
|
+
}
|
|
174
|
+
// 5. MCP: workspace settings override user settings; settings override extensions on a name clash (documented)
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
const add = (file, scope, def, doc, extra) => {
|
|
177
|
+
if (def.disabled || seen.has(def.name))
|
|
178
|
+
return;
|
|
179
|
+
seen.add(def.name);
|
|
180
|
+
items.push(...(0, common_1.mcpItems)(ctx, { agent: AGENT, def, source: file, scope, mode: "full", evidence: ev(doc), notes: ["Gemini CLI connects to each server and registers all discovered tools; schemas modelled as loaded up front (assumed)", ...extra], index: items.length }));
|
|
181
|
+
};
|
|
182
|
+
for (const def of (0, configs_1.parseMcpMap)(ws.mcpServers, (w) => ctx.warn(w)))
|
|
183
|
+
add(wsSettingsFile, "project", def, "mcp", ["workspace settings (.gemini/settings.json)"]);
|
|
184
|
+
for (const def of (0, configs_1.parseMcpMap)(user.mcpServers, (w) => ctx.warn(w)))
|
|
185
|
+
add(userSettingsFile, "user", def, "mcp", []);
|
|
186
|
+
for (const x of extServers)
|
|
187
|
+
add(x.file, "plugin", x.def, "extensions", ["from an extension"]);
|
|
188
|
+
// 6. Skills: name + description listed up front, body on activation (documented). .agents/skills wins over .gemini/skills in a tier.
|
|
189
|
+
const seenSkill = new Set();
|
|
190
|
+
const line = (r) => `- ${r.name}: ${r.desc}`;
|
|
191
|
+
const emit = (dir, scope, tag, jail) => {
|
|
192
|
+
const recs = (0, skills_1.discoverSkills)(ctx, dir, jail ? { jail } : {}).filter((r) => {
|
|
193
|
+
if (seenSkill.has(r.name)) {
|
|
194
|
+
ctx.exclude(AGENT, r.file, "a skill with the same name in a higher-precedence location wins");
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
seenSkill.add(r.name);
|
|
198
|
+
return true;
|
|
199
|
+
});
|
|
200
|
+
if (!recs.length)
|
|
201
|
+
return;
|
|
202
|
+
items.push(...(0, skills_1.skillItems)(ctx, { agent: AGENT, dir, scope, tag, recs, lineFor: line, listingMode: "always", confidence: "documented", evidence: ev("skills"), notes: ["name + description are injected at session start (documented); exact wording of the listing is assumed"] }));
|
|
203
|
+
};
|
|
204
|
+
if (ctx.cwd !== ctx.home) {
|
|
205
|
+
emit(node_path_1.default.join(ctx.cwd, ".agents", "skills"), "project", "workspace", ctx.jailFor(ctx.cwd));
|
|
206
|
+
emit(node_path_1.default.join(ctx.cwd, ".gemini", "skills"), "project", "workspace .gemini", ctx.jailFor(ctx.cwd));
|
|
207
|
+
}
|
|
208
|
+
emit(node_path_1.default.join(ctx.home, ".agents", "skills"), "user", "user");
|
|
209
|
+
emit(node_path_1.default.join(home, "skills"), "user", "user .gemini");
|
|
210
|
+
if (exts.some((e) => (0, fs_safe_1.isDir)(node_path_1.default.join(extRoot, e.name, "skills"))))
|
|
211
|
+
banner.push("skills bundled in extensions");
|
|
212
|
+
banner.push("built-in skills");
|
|
213
|
+
return { items, unmeasuredBanner: banner };
|
|
214
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.detectCopilot = detectCopilot;
|
|
7
|
+
exports.discoverCopilot = discoverCopilot;
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const spec_1 = require("../spec");
|
|
10
|
+
const fs_safe_1 = require("../fs-safe");
|
|
11
|
+
const frontmatter_1 = require("../text/frontmatter");
|
|
12
|
+
const common_1 = require("./common");
|
|
13
|
+
const configs_1 = require("../mcp/configs");
|
|
14
|
+
const AGENT = "github-copilot";
|
|
15
|
+
const ev = (doc) => (0, spec_1.evidenceFor)(AGENT, doc);
|
|
16
|
+
function copilotHome(ctx) {
|
|
17
|
+
return ctx.opts.env.COPILOT_HOME || node_path_1.default.join(ctx.home, ".copilot");
|
|
18
|
+
}
|
|
19
|
+
/** VS Code's user profile folder (where the user-level mcp.json lives). */
|
|
20
|
+
function vscodeUserDir(ctx) {
|
|
21
|
+
const plat = ctx.opts.platform ?? process.platform;
|
|
22
|
+
if (plat === "darwin")
|
|
23
|
+
return node_path_1.default.join(ctx.home, "Library", "Application Support", "Code", "User");
|
|
24
|
+
if (plat === "win32")
|
|
25
|
+
return node_path_1.default.join(ctx.opts.env.APPDATA || node_path_1.default.join(ctx.home, "AppData", "Roaming"), "Code", "User");
|
|
26
|
+
return node_path_1.default.join(ctx.opts.env.XDG_CONFIG_HOME || node_path_1.default.join(ctx.home, ".config"), "Code", "User");
|
|
27
|
+
}
|
|
28
|
+
function hasCopilotExtension(ctx) {
|
|
29
|
+
return (0, fs_safe_1.listDir)(node_path_1.default.join(ctx.home, ".vscode", "extensions")).some((e) => e.isDirectory() && /^github\.copilot(-chat)?-\d/i.test(e.name));
|
|
30
|
+
}
|
|
31
|
+
function detectCopilot(ctx) {
|
|
32
|
+
const by = [];
|
|
33
|
+
if ((0, fs_safe_1.isDir)(copilotHome(ctx)))
|
|
34
|
+
by.push("config dir");
|
|
35
|
+
if ((0, fs_safe_1.onPath)("copilot", ctx.opts.env.PATH ?? ctx.opts.env.Path, { platform: ctx.opts.platform, pathExt: ctx.opts.env.PATHEXT }))
|
|
36
|
+
by.push("binary on PATH");
|
|
37
|
+
if (hasCopilotExtension(ctx))
|
|
38
|
+
by.push("VS Code Copilot extension");
|
|
39
|
+
const repo = [];
|
|
40
|
+
const root = ctx.gitRoot ?? ctx.cwd;
|
|
41
|
+
for (const rel of [".github/copilot-instructions.md", ".github/instructions", ".github/prompts", ".vscode/mcp.json", ".github/mcp.json"]) {
|
|
42
|
+
const p = node_path_1.default.join(root, ...rel.split("/"));
|
|
43
|
+
if ((0, fs_safe_1.isFile)(p) || (0, fs_safe_1.isDir)(p))
|
|
44
|
+
repo.push(p);
|
|
45
|
+
}
|
|
46
|
+
return { detectedBy: by, repoFiles: repo };
|
|
47
|
+
}
|
|
48
|
+
const ALWAYS_NAMES = [
|
|
49
|
+
{ rel: ".github/copilot-instructions.md", imports: true, card: "copilot-instructions.md" },
|
|
50
|
+
{ rel: "AGENTS.md", imports: true, card: "AGENTS.md" },
|
|
51
|
+
{ rel: "CLAUDE.md", imports: true, card: "CLAUDE.md" },
|
|
52
|
+
{ rel: ".claude/CLAUDE.md", imports: true, card: "CLAUDE.md" },
|
|
53
|
+
{ rel: "GEMINI.md", imports: false, card: "GEMINI.md" },
|
|
54
|
+
];
|
|
55
|
+
function discoverCopilot(ctx) {
|
|
56
|
+
const items = [];
|
|
57
|
+
const home = copilotHome(ctx);
|
|
58
|
+
const root = ctx.gitRoot ?? ctx.cwd;
|
|
59
|
+
const seenText = new Map(); // trimmed content -> first path (Copilot CLI removes identical copies)
|
|
60
|
+
/** true (and recorded as excluded) when the file's content was already counted. Checked before loading so a duplicate is not analysed twice. */
|
|
61
|
+
const isDup = (f, jail) => {
|
|
62
|
+
const r = ctx.read(f, jail ? { jail, quiet: true } : { quiet: true });
|
|
63
|
+
if (!r.ok)
|
|
64
|
+
return false;
|
|
65
|
+
const k = r.text.replace(/\r\n?/g, "\n").trim();
|
|
66
|
+
if (!k)
|
|
67
|
+
return false;
|
|
68
|
+
const first = seenText.get(k);
|
|
69
|
+
if (first !== undefined) {
|
|
70
|
+
ctx.exclude(AGENT, f, `identical to ${ctx.display(first)} (Copilot CLI removes duplicate copies)`);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
seenText.set(k, f);
|
|
74
|
+
return false;
|
|
75
|
+
};
|
|
76
|
+
// User-level always-on instructions
|
|
77
|
+
const userInstr = node_path_1.default.join(home, "copilot-instructions.md");
|
|
78
|
+
const ui = isDup(userInstr) ? null : (0, common_1.loadInstruction)(ctx, userInstr, {
|
|
79
|
+
agent: AGENT, scope: "user", loadMode: "always", label: "~/.copilot/copilot-instructions.md", cardLabel: "user copilot-instructions.md",
|
|
80
|
+
stripComments: false, imports: true, importRoots: [home], importRelativeOnly: true, confidence: "documented", evidence: ev("cliInstructions"),
|
|
81
|
+
});
|
|
82
|
+
const uiKept = ui;
|
|
83
|
+
if (uiKept)
|
|
84
|
+
items.push(uiKept);
|
|
85
|
+
// Repository chain: repo root -> cwd (the "standard locations"; VS Code itself reads the root files)
|
|
86
|
+
const chain = ctx.ancestorDirs().filter((d) => (0, fs_safe_1.isInside)(d, root));
|
|
87
|
+
for (const dir of chain) {
|
|
88
|
+
const jail = ctx.jailFor(dir);
|
|
89
|
+
for (const n of ALWAYS_NAMES) {
|
|
90
|
+
const f = node_path_1.default.join(dir, ...n.rel.split("/"));
|
|
91
|
+
if (!(0, fs_safe_1.isFile)(f))
|
|
92
|
+
continue;
|
|
93
|
+
const cliOnly = n.rel.includes("CLAUDE") || n.rel === "GEMINI.md";
|
|
94
|
+
const notes = [];
|
|
95
|
+
if (dir !== root)
|
|
96
|
+
notes.push("intermediate/working directory file: read by Copilot CLI (documented); VS Code reads repository-root files");
|
|
97
|
+
if (n.rel === "GEMINI.md")
|
|
98
|
+
notes.push("read by Copilot CLI (documented); not listed for VS Code");
|
|
99
|
+
else if (cliOnly)
|
|
100
|
+
notes.push("read by Copilot CLI (documented); VS Code only with chat.useClaudeMdFile (setting not read)");
|
|
101
|
+
if (n.rel === "AGENTS.md" && dir === root)
|
|
102
|
+
notes.push("VS Code: chat.useAgentsMdFile, on by default (setting not read)");
|
|
103
|
+
if (!n.imports)
|
|
104
|
+
notes.push("@file references are not expanded in GEMINI.md by Copilot CLI (documented)");
|
|
105
|
+
if (isDup(f, jail))
|
|
106
|
+
continue;
|
|
107
|
+
const it = (0, common_1.loadInstruction)(ctx, f, {
|
|
108
|
+
agent: AGENT, scope: dir === ctx.cwd ? "project" : "ancestor", loadMode: "always", label: ctx.display(f), cardLabel: n.card, jail,
|
|
109
|
+
stripComments: false, imports: n.imports, importRoots: [root], importRelativeOnly: true,
|
|
110
|
+
confidence: "documented", evidence: ev(n.rel === ".github/copilot-instructions.md" ? "repoInstructions" : "cliInstructions"), notes,
|
|
111
|
+
});
|
|
112
|
+
const kept = it;
|
|
113
|
+
if (kept)
|
|
114
|
+
items.push(kept);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Nested agent files below cwd: Copilot CLI picks them up for files it works on there -> conditional
|
|
118
|
+
for (const hit of ctx.walk().hits) {
|
|
119
|
+
if (hit.dir === ctx.cwd || !(0, fs_safe_1.isInside)(hit.dir, ctx.cwd))
|
|
120
|
+
continue;
|
|
121
|
+
for (const name of ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]) {
|
|
122
|
+
if (!hit.names.includes(name))
|
|
123
|
+
continue;
|
|
124
|
+
const f = node_path_1.default.join(hit.dir, name);
|
|
125
|
+
if (isDup(f, ctx.jailFor(hit.dir)))
|
|
126
|
+
continue;
|
|
127
|
+
const it = (0, common_1.loadInstruction)(ctx, f, {
|
|
128
|
+
agent: AGENT, scope: "nested", loadMode: "conditional", label: ctx.display(f), cardLabel: `nested ${name}`, jail: ctx.jailFor(hit.dir),
|
|
129
|
+
stripComments: false, imports: name !== "GEMINI.md", importRoots: [root], importRelativeOnly: true, confidence: "documented", evidence: ev("cliInstructions"),
|
|
130
|
+
notes: ["applies when Copilot works on files in that directory"],
|
|
131
|
+
});
|
|
132
|
+
const kept = it;
|
|
133
|
+
if (kept)
|
|
134
|
+
items.push(kept);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Path-specific *.instructions.md (repo root and cwd .github/instructions, not intermediate dirs; user ~/.copilot/instructions)
|
|
138
|
+
const instrDirs = [[node_path_1.default.join(root, ".github", "instructions"), "project", ctx.jailFor(root)]];
|
|
139
|
+
if (ctx.cwd !== root)
|
|
140
|
+
instrDirs.push([node_path_1.default.join(ctx.cwd, ".github", "instructions"), "project", ctx.jailFor(ctx.cwd)]);
|
|
141
|
+
instrDirs.push([node_path_1.default.join(home, "instructions"), "user", undefined]);
|
|
142
|
+
for (const [dir, scope, jail] of instrDirs) {
|
|
143
|
+
if (!(0, fs_safe_1.isDir)(dir))
|
|
144
|
+
continue;
|
|
145
|
+
for (const f of (0, common_1.listFilesRec)(dir, [".instructions.md"], 500, 8)) {
|
|
146
|
+
const r = ctx.read(f, jail ? { jail } : {});
|
|
147
|
+
if (!r.ok) {
|
|
148
|
+
items.push(...[(0, common_1.loadInstruction)(ctx, f, { agent: AGENT, scope, loadMode: "conditional", label: ctx.display(f), cardLabel: "instructions file", jail, stripComments: false, imports: false, confidence: "documented", evidence: ev("pathInstructions") })].filter((x) => !!x));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const fm = (0, frontmatter_1.parseFrontmatter)(r.text);
|
|
152
|
+
const applyTo = (0, frontmatter_1.asString)(fm.data.applyTo)?.trim();
|
|
153
|
+
const desc = (0, frontmatter_1.asString)(fm.data.description)?.trim();
|
|
154
|
+
let mode;
|
|
155
|
+
const notes = [];
|
|
156
|
+
if (applyTo) {
|
|
157
|
+
mode = "conditional";
|
|
158
|
+
const pats = applyTo.split(",").map((s) => s.trim()).filter(Boolean);
|
|
159
|
+
notes.push(`applyTo: ${pats.length} pattern${pats.length > 1 ? "s" : ""}; attached when Copilot creates or changes a matching file`);
|
|
160
|
+
if (pats.some((p) => p === "**" || p === "**/*"))
|
|
161
|
+
notes.push("applyTo matches every file, so in practice it loads on nearly every edit");
|
|
162
|
+
}
|
|
163
|
+
else if (desc) {
|
|
164
|
+
mode = "deferred";
|
|
165
|
+
notes.push("no applyTo: VS Code may load it on demand when its description matches the task (documented)");
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
ctx.exclude(AGENT, f, "no applyTo and no description: only used when attached manually");
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const ex = (0, frontmatter_1.asString)(fm.data.excludeAgent);
|
|
172
|
+
if (ex)
|
|
173
|
+
notes.push(`excludeAgent: ${ex}`);
|
|
174
|
+
const it = (0, common_1.loadInstruction)(ctx, f, {
|
|
175
|
+
agent: AGENT, scope, loadMode: mode, label: ctx.display(f), cardLabel: "instructions file", jail, stripComments: false, imports: false,
|
|
176
|
+
confidence: "documented", evidence: ev("pathInstructions"), notes, group: `instr#${mode}`, groupLabel: mode === "conditional" ? "path-specific instructions (applyTo)" : "instructions loaded by description",
|
|
177
|
+
transform: (raw) => ({ text: (0, frontmatter_1.parseFrontmatter)(raw).body }), noAnalyze: true,
|
|
178
|
+
});
|
|
179
|
+
if (it)
|
|
180
|
+
items.push(it);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Prompt files: slash commands, loaded only when invoked
|
|
184
|
+
const promptDir = node_path_1.default.join(root, ".github", "prompts");
|
|
185
|
+
if ((0, fs_safe_1.isDir)(promptDir)) {
|
|
186
|
+
for (const f of (0, common_1.listFilesRec)(promptDir, [".prompt.md"], 500, 8)) {
|
|
187
|
+
const it = (0, common_1.loadInstruction)(ctx, f, {
|
|
188
|
+
agent: AGENT, scope: "project", loadMode: "on-demand", label: ctx.display(f), cardLabel: "prompt file", jail: ctx.jailFor(root), stripComments: false, imports: false,
|
|
189
|
+
confidence: "documented", evidence: ev("promptFiles"), notes: ["prompt file: loaded only when you run it as a /command"], group: "prompts", groupLabel: "prompt files (/commands)",
|
|
190
|
+
transform: (raw) => ({ text: (0, frontmatter_1.parseFrontmatter)(raw).body }), noAnalyze: true,
|
|
191
|
+
});
|
|
192
|
+
if (it)
|
|
193
|
+
items.push(it);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// MCP. Precedence (closest wins): .mcp.json/.github/mcp.json cwd -> root, then ~/.copilot/mcp-config.json; VS Code files after.
|
|
197
|
+
const seenServer = new Set();
|
|
198
|
+
const addServers = (file, scope, defs, notes, doc) => {
|
|
199
|
+
for (const def of defs) {
|
|
200
|
+
if (def.disabled || seenServer.has(def.name))
|
|
201
|
+
continue;
|
|
202
|
+
seenServer.add(def.name);
|
|
203
|
+
items.push(...(0, common_1.mcpItems)(ctx, { agent: AGENT, def, source: file, scope, mode: "full", evidence: ev(doc), notes: ["Copilot MCP tool schemas modelled as loaded up front (unverified)", ...notes], index: items.length }));
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
const readMap = (file, jail) => {
|
|
207
|
+
if (!(0, fs_safe_1.isFile)(file))
|
|
208
|
+
return undefined;
|
|
209
|
+
const r = ctx.read(file, jail ? { jail } : {});
|
|
210
|
+
if (!r.ok)
|
|
211
|
+
return undefined;
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(r.text);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
ctx.warn(`${ctx.display(file)}: could not parse as JSON (comments are not supported here), skipped`);
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const asMap = (j, key) => (j && typeof j === "object" && key in j ? j[key] : j);
|
|
221
|
+
for (const dir of [...chain].reverse()) {
|
|
222
|
+
for (const rel of [".mcp.json", ".github/mcp.json"]) {
|
|
223
|
+
if (rel === ".github/mcp.json" && dir !== root)
|
|
224
|
+
continue;
|
|
225
|
+
const f = node_path_1.default.join(dir, ...rel.split("/"));
|
|
226
|
+
const j = readMap(f, ctx.jailFor(dir));
|
|
227
|
+
if (j !== undefined)
|
|
228
|
+
addServers(f, "project", (0, configs_1.parseMcpMap)(asMap(j, "mcpServers"), (m) => ctx.warn(m)), ["project MCP config: loaded only after you trust the folder (assumed trusted)"], "cliMcp");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const userCfg = node_path_1.default.join(home, "mcp-config.json");
|
|
232
|
+
const uj = readMap(userCfg);
|
|
233
|
+
if (uj !== undefined)
|
|
234
|
+
addServers(userCfg, "user", (0, configs_1.parseMcpMap)(asMap(uj, "mcpServers"), (m) => ctx.warn(m)), [], "cliMcp");
|
|
235
|
+
for (const [f, scope] of [[node_path_1.default.join(root, ".vscode", "mcp.json"), "project"], [node_path_1.default.join(vscodeUserDir(ctx), "mcp.json"), "user"]]) {
|
|
236
|
+
const j = readMap(f, scope === "project" ? ctx.jailFor(root) : undefined);
|
|
237
|
+
if (j !== undefined)
|
|
238
|
+
addServers(f, scope, (0, configs_1.parseMcpMap)(j.servers, (m) => ctx.warn(m)), ["VS Code format (top-level \"servers\"): used by VS Code, not read by Copilot CLI (documented)"], "vscodeMcp");
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
items,
|
|
242
|
+
unmeasuredBanner: [
|
|
243
|
+
"built-in system prompt and tools (incl. the built-in GitHub MCP server)",
|
|
244
|
+
"organization / enterprise instructions",
|
|
245
|
+
"VS Code settings (instruction files toggles, settings-based instructions) are not read; defaults assumed",
|
|
246
|
+
"skills and custom agents (not modelled yet)",
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ADAPTERS = void 0;
|
|
4
|
+
const claude_code_1 = require("./claude-code");
|
|
5
|
+
const cursor_1 = require("./cursor");
|
|
6
|
+
const codex_1 = require("./codex");
|
|
7
|
+
const github_copilot_1 = require("./github-copilot");
|
|
8
|
+
const gemini_cli_1 = require("./gemini-cli");
|
|
9
|
+
/** One adapter per id in src/agents.ts. A missing entry is a compile error. */
|
|
10
|
+
exports.ADAPTERS = {
|
|
11
|
+
"claude-code": { detect: claude_code_1.detectClaude, discover: claude_code_1.discoverClaude },
|
|
12
|
+
cursor: { detect: cursor_1.detectCursor, discover: cursor_1.discoverCursor },
|
|
13
|
+
codex: { detect: codex_1.detectCodex, discover: codex_1.discoverCodex },
|
|
14
|
+
"github-copilot": { detect: github_copilot_1.detectCopilot, discover: github_copilot_1.discoverCopilot },
|
|
15
|
+
"gemini-cli": { detect: gemini_cli_1.detectGemini, discover: gemini_cli_1.discoverGemini },
|
|
16
|
+
};
|
package/dist/adapters/skills.js
CHANGED
|
@@ -101,6 +101,16 @@ function skillItems(ctx, s) {
|
|
|
101
101
|
group: s.group,
|
|
102
102
|
});
|
|
103
103
|
it.breakdown = per;
|
|
104
|
+
if (it.tokens && (s.lowLineFor || s.highLineFor)) {
|
|
105
|
+
const alt = (fn, tag) => {
|
|
106
|
+
const t = listed.map((l) => fn(l.r)).join("\n") + "\n";
|
|
107
|
+
return prof.apply(ctx.count(`skills-${tag}|${s.agent}|${s.dir}|${t.length}`, t));
|
|
108
|
+
};
|
|
109
|
+
if (s.lowLineFor)
|
|
110
|
+
it.tokens.low = Math.min(it.tokens.low, alt(s.lowLineFor, "lo").low);
|
|
111
|
+
if (s.highLineFor)
|
|
112
|
+
it.tokens.high = Math.max(it.tokens.high, alt(s.highLineFor, "hi").high);
|
|
113
|
+
}
|
|
104
114
|
// "What to cut" parts: one per plugin (or, without a plugin, the whole group), scaled so they add up to the item's estimate.
|
|
105
115
|
if (it.tokens) {
|
|
106
116
|
const groups = new Map();
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AGENT_NAMES = exports.ALL_AGENTS = exports.AGENTS = void 0;
|
|
4
|
+
exports.agentMeta = agentMeta;
|
|
5
|
+
exports.AGENTS = [
|
|
6
|
+
{
|
|
7
|
+
id: "claude-code",
|
|
8
|
+
name: "Claude Code",
|
|
9
|
+
tokenFamily: "claude",
|
|
10
|
+
lookedFor: "~/.claude, ~/.claude.json, `claude` on PATH; CLAUDE.md, .claude/, .mcp.json in the project",
|
|
11
|
+
whereLooked: "CLAUDE.md (project, parent folders, ~/.claude), .claude/{rules,skills,commands}, plugins, .mcp.json, ~/.claude.json",
|
|
12
|
+
projectMarkers: ["CLAUDE.md", "CLAUDE.local.md", ".mcp.json", ".claude/"],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "cursor",
|
|
16
|
+
name: "Cursor",
|
|
17
|
+
tokenFamily: "mixed",
|
|
18
|
+
lookedFor: "~/.cursor, Cursor.app, `cursor` on PATH; .cursor/, .cursorrules, AGENTS.md in the project",
|
|
19
|
+
whereLooked: ".cursor/rules (incl. nested), AGENTS.md, .cursorrules, skills folders, ~/.cursor/{skills,plugins,mcp.json}, .cursor/mcp.json",
|
|
20
|
+
hint: "Cursor's User Rules live inside the app, not on disk. Paste them into a file and run with --paste-user-rules <file> to count them.",
|
|
21
|
+
cardChips: ["unmeasured: User Rules"],
|
|
22
|
+
projectMarkers: [".cursorrules", "AGENTS.md", ".cursor/"],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "codex",
|
|
26
|
+
name: "Codex",
|
|
27
|
+
tokenFamily: "openai",
|
|
28
|
+
lookedFor: "~/.codex, `codex` on PATH; AGENTS.md, .codex/, .agents/skills in the project",
|
|
29
|
+
whereLooked: "AGENTS.md chain, ~/.codex/{AGENTS.md,config.toml}, .agents/skills, ~/.agents/skills, .codex/config.toml",
|
|
30
|
+
projectMarkers: ["AGENTS.md", ".codex/", ".agents/"],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "github-copilot",
|
|
34
|
+
name: "GitHub Copilot",
|
|
35
|
+
tokenFamily: "mixed",
|
|
36
|
+
lookedFor: "~/.copilot, `copilot` on PATH, the VS Code Copilot Chat extension; .github/copilot-instructions.md, .github/instructions/, .github/prompts/, .vscode/mcp.json in the project",
|
|
37
|
+
whereLooked: ".github/copilot-instructions.md, AGENTS.md/CLAUDE.md/GEMINI.md (repo root → cwd), .github/instructions/**, .github/prompts/**, ~/.copilot/{copilot-instructions.md,instructions,mcp-config.json}, .mcp.json, .github/mcp.json, .vscode/mcp.json, VS Code user mcp.json",
|
|
38
|
+
hint: "Organization instructions and VS Code settings-based instructions live on GitHub or in VS Code settings, not in the scanned files.",
|
|
39
|
+
projectMarkers: [".github/copilot-instructions.md", ".github/instructions/", ".vscode/mcp.json"],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "gemini-cli",
|
|
43
|
+
name: "Gemini CLI",
|
|
44
|
+
tokenFamily: "gemini",
|
|
45
|
+
lookedFor: "~/.gemini, `gemini` on PATH; GEMINI.md, .gemini/ in the project",
|
|
46
|
+
whereLooked: "~/.gemini/GEMINI.md, GEMINI.md (cwd → project root, and below cwd), context.fileName, ~/.gemini/extensions/*, ~/.gemini/settings.json, .gemini/settings.json, .gemini/skills, .agents/skills",
|
|
47
|
+
projectMarkers: ["GEMINI.md", ".gemini/"],
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
exports.ALL_AGENTS = exports.AGENTS.map((a) => a.id);
|
|
51
|
+
exports.AGENT_NAMES = Object.fromEntries(exports.AGENTS.map((a) => [a.id, a.name]));
|
|
52
|
+
function agentMeta(id) {
|
|
53
|
+
return exports.AGENTS.find((a) => a.id === id);
|
|
54
|
+
}
|