contextos-agents 1.5.0 → 1.6.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/.agents/adapters/cursor/export.js +34 -37
- package/.agents/core/profiles/mvp.yaml +1 -0
- package/.agents/ctx.js +37 -5
- package/.agents/doctor.js +269 -0
- package/.agents/mcp/runtime.py +17 -1
- package/.agents/mcp/server.mjs +43 -145
- package/.agents/plugins.js +21 -11
- package/.agents/resolver.js +112 -1
- package/.agents/stats.js +133 -0
- package/.agents/validate.js +53 -0
- package/.agents/watch.js +161 -0
- package/README.md +123 -53
- package/bin/index.js +186 -60
- package/package.json +10 -5
package/.agents/mcp/server.mjs
CHANGED
|
@@ -2240,8 +2240,8 @@ var require_resolve = __commonJS({
|
|
|
2240
2240
|
}
|
|
2241
2241
|
return count;
|
|
2242
2242
|
}
|
|
2243
|
-
function getFullPath(resolver, id = "",
|
|
2244
|
-
if (
|
|
2243
|
+
function getFullPath(resolver, id = "", normalize3) {
|
|
2244
|
+
if (normalize3 !== false)
|
|
2245
2245
|
id = normalizeId(id);
|
|
2246
2246
|
const p5 = resolver.parse(id);
|
|
2247
2247
|
return _getFullPath(resolver, p5);
|
|
@@ -3581,7 +3581,7 @@ var require_fast_uri = __commonJS({
|
|
|
3581
3581
|
"use strict";
|
|
3582
3582
|
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
|
|
3583
3583
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
3584
|
-
function
|
|
3584
|
+
function normalize3(uri, options) {
|
|
3585
3585
|
if (typeof uri === "string") {
|
|
3586
3586
|
uri = /** @type {T} */
|
|
3587
3587
|
serialize(parse3(uri, options), options);
|
|
@@ -3817,7 +3817,7 @@ var require_fast_uri = __commonJS({
|
|
|
3817
3817
|
}
|
|
3818
3818
|
var fastUri = {
|
|
3819
3819
|
SCHEMES,
|
|
3820
|
-
normalize:
|
|
3820
|
+
normalize: normalize3,
|
|
3821
3821
|
resolve: resolve10,
|
|
3822
3822
|
resolveComponent,
|
|
3823
3823
|
equal,
|
|
@@ -6833,9 +6833,10 @@ var init_provider = __esm({
|
|
|
6833
6833
|
// src/security/secret-filter.ts
|
|
6834
6834
|
import * as path3 from "node:path";
|
|
6835
6835
|
function isBlockedPath(filePath) {
|
|
6836
|
-
const
|
|
6837
|
-
const
|
|
6838
|
-
const
|
|
6836
|
+
const normalized = path3.normalize(filePath);
|
|
6837
|
+
const basename5 = path3.basename(normalized).toLowerCase();
|
|
6838
|
+
const ext = path3.extname(normalized).toLowerCase();
|
|
6839
|
+
const parts = normalized.split(/[/\\]/).map((p5) => p5.toLowerCase());
|
|
6839
6840
|
if (BLOCKED_FILENAMES.some((f7) => basename5 === f7.toLowerCase())) {
|
|
6840
6841
|
return true;
|
|
6841
6842
|
}
|
|
@@ -6851,7 +6852,8 @@ function isBlockedPath(filePath) {
|
|
|
6851
6852
|
return false;
|
|
6852
6853
|
}
|
|
6853
6854
|
function containsSecrets(content) {
|
|
6854
|
-
|
|
6855
|
+
if (!content) return false;
|
|
6856
|
+
const sample = content.length > 1e6 ? content.slice(0, 1e6) : content;
|
|
6855
6857
|
return SECRET_CONTENT_PATTERNS.some((pattern) => pattern.test(sample));
|
|
6856
6858
|
}
|
|
6857
6859
|
function redactSecrets(text) {
|
|
@@ -6875,7 +6877,10 @@ var init_secret_filter = __esm({
|
|
|
6875
6877
|
"credentials",
|
|
6876
6878
|
"credentials.json",
|
|
6877
6879
|
"service-account.json",
|
|
6878
|
-
"serviceAccountKey.json"
|
|
6880
|
+
"serviceAccountKey.json",
|
|
6881
|
+
".netrc",
|
|
6882
|
+
".git-credentials",
|
|
6883
|
+
".npmrc"
|
|
6879
6884
|
];
|
|
6880
6885
|
BLOCKED_EXTENSIONS = [".pem", ".key", ".p12", ".pfx", ".jks", ".keystore"];
|
|
6881
6886
|
BLOCKED_DIRS = [".ssh", ".gnupg", ".aws", ".azure", ".gcp", ".kube"];
|
|
@@ -6887,12 +6892,18 @@ var init_secret_filter = __esm({
|
|
|
6887
6892
|
// GitHub personal access token
|
|
6888
6893
|
/gho_[A-Za-z0-9]{36}/,
|
|
6889
6894
|
// GitHub OAuth token
|
|
6895
|
+
/github_pat_[A-Za-z0-9_]{82}/,
|
|
6896
|
+
// GitHub fine-grained PAT
|
|
6890
6897
|
/sk-[A-Za-z0-9]{32,}/,
|
|
6891
6898
|
// OpenAI API key
|
|
6892
6899
|
/sk-ant-[A-Za-z0-9-]{90,}/,
|
|
6893
6900
|
// Anthropic API key
|
|
6894
|
-
/AIza[A-Za-z0-9_-]{35}
|
|
6901
|
+
/AIza[A-Za-z0-9_-]{35}/,
|
|
6895
6902
|
// Google API key
|
|
6903
|
+
/npm_[A-Za-z0-9]{32,36}/,
|
|
6904
|
+
// npm access token
|
|
6905
|
+
/xox[baprs]-[A-Za-z0-9_-]{10,48}/
|
|
6906
|
+
// Slack token
|
|
6896
6907
|
];
|
|
6897
6908
|
}
|
|
6898
6909
|
});
|
|
@@ -41559,121 +41570,6 @@ var require_browser = __commonJS({
|
|
|
41559
41570
|
}
|
|
41560
41571
|
});
|
|
41561
41572
|
|
|
41562
|
-
// ../node_modules/has-flag/index.js
|
|
41563
|
-
var require_has_flag = __commonJS({
|
|
41564
|
-
"../node_modules/has-flag/index.js"(exports, module) {
|
|
41565
|
-
"use strict";
|
|
41566
|
-
module.exports = (flag, argv = process.argv) => {
|
|
41567
|
-
const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
|
|
41568
|
-
const position = argv.indexOf(prefix + flag);
|
|
41569
|
-
const terminatorPosition = argv.indexOf("--");
|
|
41570
|
-
return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
|
|
41571
|
-
};
|
|
41572
|
-
}
|
|
41573
|
-
});
|
|
41574
|
-
|
|
41575
|
-
// ../node_modules/supports-color/index.js
|
|
41576
|
-
var require_supports_color = __commonJS({
|
|
41577
|
-
"../node_modules/supports-color/index.js"(exports, module) {
|
|
41578
|
-
"use strict";
|
|
41579
|
-
var os7 = __require("os");
|
|
41580
|
-
var tty = __require("tty");
|
|
41581
|
-
var hasFlag = require_has_flag();
|
|
41582
|
-
var { env } = process;
|
|
41583
|
-
var forceColor;
|
|
41584
|
-
if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
|
|
41585
|
-
forceColor = 0;
|
|
41586
|
-
} else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
|
|
41587
|
-
forceColor = 1;
|
|
41588
|
-
}
|
|
41589
|
-
if ("FORCE_COLOR" in env) {
|
|
41590
|
-
if (env.FORCE_COLOR === "true") {
|
|
41591
|
-
forceColor = 1;
|
|
41592
|
-
} else if (env.FORCE_COLOR === "false") {
|
|
41593
|
-
forceColor = 0;
|
|
41594
|
-
} else {
|
|
41595
|
-
forceColor = env.FORCE_COLOR.length === 0 ? 1 : Math.min(parseInt(env.FORCE_COLOR, 10), 3);
|
|
41596
|
-
}
|
|
41597
|
-
}
|
|
41598
|
-
function translateLevel(level) {
|
|
41599
|
-
if (level === 0) {
|
|
41600
|
-
return false;
|
|
41601
|
-
}
|
|
41602
|
-
return {
|
|
41603
|
-
level,
|
|
41604
|
-
hasBasic: true,
|
|
41605
|
-
has256: level >= 2,
|
|
41606
|
-
has16m: level >= 3
|
|
41607
|
-
};
|
|
41608
|
-
}
|
|
41609
|
-
function supportsColor(haveStream, streamIsTTY) {
|
|
41610
|
-
if (forceColor === 0) {
|
|
41611
|
-
return 0;
|
|
41612
|
-
}
|
|
41613
|
-
if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
|
|
41614
|
-
return 3;
|
|
41615
|
-
}
|
|
41616
|
-
if (hasFlag("color=256")) {
|
|
41617
|
-
return 2;
|
|
41618
|
-
}
|
|
41619
|
-
if (haveStream && !streamIsTTY && forceColor === void 0) {
|
|
41620
|
-
return 0;
|
|
41621
|
-
}
|
|
41622
|
-
const min = forceColor || 0;
|
|
41623
|
-
if (env.TERM === "dumb") {
|
|
41624
|
-
return min;
|
|
41625
|
-
}
|
|
41626
|
-
if (process.platform === "win32") {
|
|
41627
|
-
const osRelease = os7.release().split(".");
|
|
41628
|
-
if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
|
|
41629
|
-
return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
41630
|
-
}
|
|
41631
|
-
return 1;
|
|
41632
|
-
}
|
|
41633
|
-
if ("CI" in env) {
|
|
41634
|
-
if (["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "GITHUB_ACTIONS", "BUILDKITE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
|
|
41635
|
-
return 1;
|
|
41636
|
-
}
|
|
41637
|
-
return min;
|
|
41638
|
-
}
|
|
41639
|
-
if ("TEAMCITY_VERSION" in env) {
|
|
41640
|
-
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
|
|
41641
|
-
}
|
|
41642
|
-
if (env.COLORTERM === "truecolor") {
|
|
41643
|
-
return 3;
|
|
41644
|
-
}
|
|
41645
|
-
if ("TERM_PROGRAM" in env) {
|
|
41646
|
-
const version2 = parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
|
|
41647
|
-
switch (env.TERM_PROGRAM) {
|
|
41648
|
-
case "iTerm.app":
|
|
41649
|
-
return version2 >= 3 ? 3 : 2;
|
|
41650
|
-
case "Apple_Terminal":
|
|
41651
|
-
return 2;
|
|
41652
|
-
}
|
|
41653
|
-
}
|
|
41654
|
-
if (/-256(color)?$/i.test(env.TERM)) {
|
|
41655
|
-
return 2;
|
|
41656
|
-
}
|
|
41657
|
-
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
|
|
41658
|
-
return 1;
|
|
41659
|
-
}
|
|
41660
|
-
if ("COLORTERM" in env) {
|
|
41661
|
-
return 1;
|
|
41662
|
-
}
|
|
41663
|
-
return min;
|
|
41664
|
-
}
|
|
41665
|
-
function getSupportLevel(stream2) {
|
|
41666
|
-
const level = supportsColor(stream2, stream2 && stream2.isTTY);
|
|
41667
|
-
return translateLevel(level);
|
|
41668
|
-
}
|
|
41669
|
-
module.exports = {
|
|
41670
|
-
supportsColor: getSupportLevel,
|
|
41671
|
-
stdout: translateLevel(supportsColor(true, tty.isatty(1))),
|
|
41672
|
-
stderr: translateLevel(supportsColor(true, tty.isatty(2)))
|
|
41673
|
-
};
|
|
41674
|
-
}
|
|
41675
|
-
});
|
|
41676
|
-
|
|
41677
41573
|
// node_modules/debug/src/node.js
|
|
41678
41574
|
var require_node = __commonJS({
|
|
41679
41575
|
"node_modules/debug/src/node.js"(exports, module) {
|
|
@@ -41692,7 +41588,7 @@ var require_node = __commonJS({
|
|
|
41692
41588
|
);
|
|
41693
41589
|
exports.colors = [6, 2, 3, 4, 5, 1];
|
|
41694
41590
|
try {
|
|
41695
|
-
const supportsColor =
|
|
41591
|
+
const supportsColor = __require("supports-color");
|
|
41696
41592
|
if (supportsColor && (supportsColor.stderr || supportsColor).level >= 2) {
|
|
41697
41593
|
exports.colors = [
|
|
41698
41594
|
20,
|
|
@@ -118880,7 +118776,7 @@ var require_util5 = __commonJS({
|
|
|
118880
118776
|
return url2;
|
|
118881
118777
|
}
|
|
118882
118778
|
exports.urlGenerate = urlGenerate;
|
|
118883
|
-
function
|
|
118779
|
+
function normalize3(aPath) {
|
|
118884
118780
|
var path16 = aPath;
|
|
118885
118781
|
var url2 = urlParse(aPath);
|
|
118886
118782
|
if (url2) {
|
|
@@ -118917,7 +118813,7 @@ var require_util5 = __commonJS({
|
|
|
118917
118813
|
}
|
|
118918
118814
|
return path16;
|
|
118919
118815
|
}
|
|
118920
|
-
exports.normalize =
|
|
118816
|
+
exports.normalize = normalize3;
|
|
118921
118817
|
function join10(aRoot, aPath) {
|
|
118922
118818
|
if (aRoot === "") {
|
|
118923
118819
|
aRoot = ".";
|
|
@@ -118943,7 +118839,7 @@ var require_util5 = __commonJS({
|
|
|
118943
118839
|
aRootUrl.host = aPath;
|
|
118944
118840
|
return urlGenerate(aRootUrl);
|
|
118945
118841
|
}
|
|
118946
|
-
var joined = aPath.charAt(0) === "/" ? aPath :
|
|
118842
|
+
var joined = aPath.charAt(0) === "/" ? aPath : normalize3(aRoot.replace(/\/+$/, "") + "/" + aPath);
|
|
118947
118843
|
if (aRootUrl) {
|
|
118948
118844
|
aRootUrl.path = joined;
|
|
118949
118845
|
return urlGenerate(aRootUrl);
|
|
@@ -119125,7 +119021,7 @@ var require_util5 = __commonJS({
|
|
|
119125
119021
|
}
|
|
119126
119022
|
sourceURL = join10(urlGenerate(parsed), sourceURL);
|
|
119127
119023
|
}
|
|
119128
|
-
return
|
|
119024
|
+
return normalize3(sourceURL);
|
|
119129
119025
|
}
|
|
119130
119026
|
exports.computeSourceURL = computeSourceURL;
|
|
119131
119027
|
}
|
|
@@ -162826,7 +162722,12 @@ function writeCodeBlocks(workDir, blocks) {
|
|
|
162826
162722
|
for (const block of blocks) {
|
|
162827
162723
|
const normalized = path13.normalize(block.filePath);
|
|
162828
162724
|
if (normalized.startsWith("..") || path13.isAbsolute(normalized)) {
|
|
162829
|
-
process.stderr.write(`[contextos-agent] Skipping unsafe path: ${block.filePath}
|
|
162725
|
+
process.stderr.write(`[contextos-agent] Skipping unsafe path (traversal): ${block.filePath}
|
|
162726
|
+
`);
|
|
162727
|
+
continue;
|
|
162728
|
+
}
|
|
162729
|
+
if (isBlockedPath(normalized) || !isPathAllowed(normalized, workDir)) {
|
|
162730
|
+
process.stderr.write(`[contextos-agent] Skipping forbidden/policy-blocked path: ${block.filePath}
|
|
162830
162731
|
`);
|
|
162831
162732
|
continue;
|
|
162832
162733
|
}
|
|
@@ -188780,20 +188681,8 @@ function buildContextPrompt(projectRoot, task, options = {}) {
|
|
|
188780
188681
|
// src/mcp/tools/contextos.ts
|
|
188781
188682
|
init_secret_filter();
|
|
188782
188683
|
init_command_exists();
|
|
188783
|
-
var ALLOWED_VERIFY_TOOLS = /* @__PURE__ */ new Set([
|
|
188784
|
-
|
|
188785
|
-
"npx",
|
|
188786
|
-
"pnpm",
|
|
188787
|
-
"yarn",
|
|
188788
|
-
"pytest",
|
|
188789
|
-
"python",
|
|
188790
|
-
"python3",
|
|
188791
|
-
"cargo",
|
|
188792
|
-
"go",
|
|
188793
|
-
"vitest",
|
|
188794
|
-
"jest",
|
|
188795
|
-
"node"
|
|
188796
|
-
]);
|
|
188684
|
+
var ALLOWED_VERIFY_TOOLS = /* @__PURE__ */ new Set(["npm", "npx", "pnpm", "yarn", "pytest", "cargo", "go", "vitest", "jest"]);
|
|
188685
|
+
var FORBIDDEN_VERIFY_ARGS = /* @__PURE__ */ new Set(["-e", "-c", "--eval", "--print", "-p", "--input-type"]);
|
|
188797
188686
|
var STRICT_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
188798
188687
|
"PATH",
|
|
188799
188688
|
"HOME",
|
|
@@ -188846,6 +188735,15 @@ async function runWorktreeVerification(worktreePath, command) {
|
|
|
188846
188735
|
output: `Security error: Command "${rawTool}" is not in the allowed verification tools whitelist (${Array.from(ALLOWED_VERIFY_TOOLS).join(", ")})`
|
|
188847
188736
|
};
|
|
188848
188737
|
}
|
|
188738
|
+
const args = parts.slice(1);
|
|
188739
|
+
for (const arg of args) {
|
|
188740
|
+
if (FORBIDDEN_VERIFY_ARGS.has(arg.toLowerCase())) {
|
|
188741
|
+
return {
|
|
188742
|
+
verified: false,
|
|
188743
|
+
output: `Security error: Argument "${arg}" is not permitted in verify_command`
|
|
188744
|
+
};
|
|
188745
|
+
}
|
|
188746
|
+
}
|
|
188849
188747
|
const exePath = await resolveExecutablePath(rawTool);
|
|
188850
188748
|
const targetExecutable = exePath || rawTool;
|
|
188851
188749
|
return new Promise((resolveResult) => {
|
package/.agents/plugins.js
CHANGED
|
@@ -43,14 +43,20 @@ const PROMPT_INJECTION_PATTERNS = [
|
|
|
43
43
|
/you\s+are\s+now/i,
|
|
44
44
|
/system\s*:\s*/i,
|
|
45
45
|
/\[SYSTEM\]/i,
|
|
46
|
+
/base64\s*,\s*[A-Za-z0-9+/=]{40,}/i,
|
|
47
|
+
/https?:\/\/[^\s)]+\/(?:exfil|steal|leak|webhook|collect)/i,
|
|
48
|
+
/\b(?:eval|exec|Function)\s*\(/i,
|
|
46
49
|
];
|
|
47
50
|
|
|
48
|
-
function scanForPromptInjection(content) {
|
|
51
|
+
function scanForPromptInjection(content, options = {}) {
|
|
49
52
|
if (typeof content !== 'string') return false;
|
|
50
53
|
const found = PROMPT_INJECTION_PATTERNS.filter(p => p.test(content));
|
|
51
54
|
if (found.length > 0) {
|
|
52
|
-
console.warn(c.
|
|
53
|
-
console.warn(c.yellow(`
|
|
55
|
+
console.warn(c.red(' 🚨 Potential prompt injection or unsafe pattern detected in skill:'));
|
|
56
|
+
console.warn(c.yellow(` Pattern matches: ${found.map(p => p.source).join(', ')}`));
|
|
57
|
+
if (options.throwOnMatch) {
|
|
58
|
+
throw new Error(`Security validation failed: prompt injection detected (${found.length} pattern matches). Installation aborted. Pass --force-unsafe-prompts to override.`);
|
|
59
|
+
}
|
|
54
60
|
return true;
|
|
55
61
|
}
|
|
56
62
|
return false;
|
|
@@ -166,7 +172,7 @@ function parseRef(ref) {
|
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
// ── GitHub installer ──────────────────────────────────────────────────────────
|
|
169
|
-
async function installFromGitHub(descriptor, skillName, dryRun, checksum) {
|
|
175
|
+
async function installFromGitHub(descriptor, skillName, dryRun, checksum, forceUnsafe = false) {
|
|
170
176
|
const { owner, repo, gitRef, isPinned, subPath } = descriptor;
|
|
171
177
|
const skillPath = subPath || '';
|
|
172
178
|
const base = `https://raw.githubusercontent.com/${owner}/${repo}/${gitRef}`;
|
|
@@ -198,7 +204,7 @@ async function installFromGitHub(descriptor, skillName, dryRun, checksum) {
|
|
|
198
204
|
if (checksum && sha256.toLowerCase() !== checksum.toLowerCase()) {
|
|
199
205
|
throw new Error(`Checksum mismatch for ${skillName}: expected ${checksum}, got ${sha256}`);
|
|
200
206
|
}
|
|
201
|
-
scanForPromptInjection(skillMdContent);
|
|
207
|
+
scanForPromptInjection(skillMdContent, { throwOnMatch: !forceUnsafe });
|
|
202
208
|
|
|
203
209
|
// Optionally fetch skill.yaml if it exists
|
|
204
210
|
const yamlUrl = skillPath
|
|
@@ -244,7 +250,7 @@ async function installFromGitHub(descriptor, skillName, dryRun, checksum) {
|
|
|
244
250
|
}
|
|
245
251
|
|
|
246
252
|
// ── npm installer ─────────────────────────────────────────────────────────────
|
|
247
|
-
function installFromNpm(descriptor, skillName, dryRun, checksum) {
|
|
253
|
+
function installFromNpm(descriptor, skillName, dryRun, checksum, forceUnsafe = false) {
|
|
248
254
|
const pkgName = descriptor.package;
|
|
249
255
|
|
|
250
256
|
if (dryRun) {
|
|
@@ -280,7 +286,7 @@ function installFromNpm(descriptor, skillName, dryRun, checksum) {
|
|
|
280
286
|
if (checksum && sha256.toLowerCase() !== checksum.toLowerCase()) {
|
|
281
287
|
throw new Error(`Checksum mismatch for ${skillName}: expected ${checksum}, got ${sha256}`);
|
|
282
288
|
}
|
|
283
|
-
scanForPromptInjection(skillMdContent);
|
|
289
|
+
scanForPromptInjection(skillMdContent, { throwOnMatch: !forceUnsafe });
|
|
284
290
|
|
|
285
291
|
const targetDir = path.join(PLUGINS_DIR, skillName);
|
|
286
292
|
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
@@ -321,9 +327,13 @@ function isSafeSkillName(skillName) {
|
|
|
321
327
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
322
328
|
|
|
323
329
|
/**
|
|
324
|
-
* skill add <ref> [--dry-run] [--checksum <sha256>]
|
|
330
|
+
* skill add <ref> [--dry-run] [--checksum <sha256>] [--force-unsafe-prompts]
|
|
325
331
|
*/
|
|
326
|
-
async function add(ref,
|
|
332
|
+
async function add(ref, options = {}) {
|
|
333
|
+
const dryRun = Boolean(options.dryRun || options['dry-run']);
|
|
334
|
+
const checksum = options.checksum || null;
|
|
335
|
+
const forceUnsafe = Boolean(options.forceUnsafe || options['force-unsafe-prompts']);
|
|
336
|
+
|
|
327
337
|
if (!ref) {
|
|
328
338
|
console.error(c.red('Usage: ctx.js skill add <ref> [--checksum <sha256>]'));
|
|
329
339
|
console.error(' ref can be: username/repo, username/repo/path/to/skill, or npm-package-name');
|
|
@@ -361,9 +371,9 @@ async function add(ref, { dryRun = false, checksum } = {}) {
|
|
|
361
371
|
let installedSha256 = null;
|
|
362
372
|
try {
|
|
363
373
|
if (descriptor.type === 'github') {
|
|
364
|
-
installedSha256 = await installFromGitHub(descriptor, skillName, dryRun, checksum);
|
|
374
|
+
installedSha256 = await installFromGitHub(descriptor, skillName, dryRun, checksum, forceUnsafe);
|
|
365
375
|
} else {
|
|
366
|
-
installedSha256 = installFromNpm(descriptor, skillName, dryRun, checksum);
|
|
376
|
+
installedSha256 = installFromNpm(descriptor, skillName, dryRun, checksum, forceUnsafe);
|
|
367
377
|
}
|
|
368
378
|
} catch (err) {
|
|
369
379
|
console.error(c.red(`\n[ERROR] Failed to install '${skillName}': ${err.message}`));
|
package/.agents/resolver.js
CHANGED
|
@@ -225,13 +225,115 @@ function buildSkillIndex(projectDir = process.cwd()) {
|
|
|
225
225
|
return index;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Phase 2: AST & Project Dependency Graph Analysis.
|
|
230
|
+
* Scans project manifests and configs to detect active technologies
|
|
231
|
+
* that keyword or regex matching in prompts might omit.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} [projectDir=process.cwd()] - Project root directory
|
|
234
|
+
* @returns {Map<string, number>} Map of skill name to score weight
|
|
235
|
+
*/
|
|
236
|
+
function analyzeImportGraph(projectDir = process.cwd()) {
|
|
237
|
+
const techSignals = new Map();
|
|
238
|
+
if (!projectDir || !fs.existsSync(projectDir)) return techSignals;
|
|
239
|
+
|
|
240
|
+
const addSignal = (skill, weight) => {
|
|
241
|
+
techSignals.set(skill, (techSignals.get(skill) || 0) + weight);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// 1. Scan package.json dependencies and devDependencies
|
|
245
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
246
|
+
if (fs.existsSync(pkgPath)) {
|
|
247
|
+
try {
|
|
248
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
249
|
+
const allDeps = {
|
|
250
|
+
...(pkg.dependencies || {}),
|
|
251
|
+
...(pkg.devDependencies || {}),
|
|
252
|
+
...(pkg.peerDependencies || {}),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const DEP_SKILL_MAP = {
|
|
256
|
+
'react': 'react',
|
|
257
|
+
'react-dom': 'react',
|
|
258
|
+
'next': 'nextjs',
|
|
259
|
+
'prisma': 'database',
|
|
260
|
+
'@prisma/client': 'database',
|
|
261
|
+
'drizzle-orm': 'database',
|
|
262
|
+
'drizzle-kit': 'database',
|
|
263
|
+
'typeorm': 'database',
|
|
264
|
+
'mongoose': 'database',
|
|
265
|
+
'pg': 'database',
|
|
266
|
+
'mysql2': 'database',
|
|
267
|
+
'vitest': 'testing',
|
|
268
|
+
'jest': 'testing',
|
|
269
|
+
'playwright': 'testing',
|
|
270
|
+
'@playwright/test': 'testing',
|
|
271
|
+
'cypress': 'testing',
|
|
272
|
+
'express': 'node',
|
|
273
|
+
'fastify': 'node',
|
|
274
|
+
'hono': 'node',
|
|
275
|
+
'@nestjs/core': 'nestjs',
|
|
276
|
+
'typescript': 'typescript',
|
|
277
|
+
'zod': 'typescript',
|
|
278
|
+
'tailwindcss': 'ui-ux-pro',
|
|
279
|
+
'lucide-react': 'ui-ux-pro',
|
|
280
|
+
'@radix-ui/react-dialog': 'web-accessibility',
|
|
281
|
+
'framer-motion': 'impeccable-design',
|
|
282
|
+
'dockerode': 'docker',
|
|
283
|
+
'ioredis': 'system-design',
|
|
284
|
+
'kafkajs': 'microservices',
|
|
285
|
+
'amqplib': 'microservices',
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
for (const [dep, skill] of Object.entries(DEP_SKILL_MAP)) {
|
|
289
|
+
if (allDeps[dep]) {
|
|
290
|
+
addSignal(skill, 15);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// ignore malformed package.json
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 2. Scan framework & tooling configuration files
|
|
299
|
+
const CONFIG_FILE_MAP = [
|
|
300
|
+
{ file: 'tsconfig.json', skill: 'typescript', weight: 10 },
|
|
301
|
+
{ file: 'next.config.js', skill: 'nextjs', weight: 15 },
|
|
302
|
+
{ file: 'next.config.mjs', skill: 'nextjs', weight: 15 },
|
|
303
|
+
{ file: 'next.config.ts', skill: 'nextjs', weight: 15 },
|
|
304
|
+
{ file: 'tailwind.config.js', skill: 'ui-ux-pro', weight: 10 },
|
|
305
|
+
{ file: 'tailwind.config.ts', skill: 'ui-ux-pro', weight: 10 },
|
|
306
|
+
{ file: 'nest-cli.json', skill: 'nestjs', weight: 15 },
|
|
307
|
+
{ file: 'prisma/schema.prisma', skill: 'database', weight: 15 },
|
|
308
|
+
{ file: 'drizzle.config.ts', skill: 'database', weight: 15 },
|
|
309
|
+
{ file: 'drizzle.config.js', skill: 'database', weight: 15 },
|
|
310
|
+
{ file: 'Dockerfile', skill: 'docker', weight: 15 },
|
|
311
|
+
{ file: 'docker-compose.yml', skill: 'docker', weight: 15 },
|
|
312
|
+
{ file: 'docker-compose.yaml', skill: 'docker', weight: 15 },
|
|
313
|
+
{ file: 'requirements.txt', skill: 'fastapi', weight: 15 },
|
|
314
|
+
{ file: 'pyproject.toml', skill: 'fastapi', weight: 15 },
|
|
315
|
+
{ file: 'vitest.config.ts', skill: 'testing', weight: 15 },
|
|
316
|
+
{ file: 'vitest.config.js', skill: 'testing', weight: 15 },
|
|
317
|
+
{ file: 'playwright.config.ts', skill: 'testing', weight: 15 },
|
|
318
|
+
{ file: 'playwright.config.js', skill: 'testing', weight: 15 },
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
for (const { file, skill, weight } of CONFIG_FILE_MAP) {
|
|
322
|
+
if (fs.existsSync(path.join(projectDir, file))) {
|
|
323
|
+
addSignal(skill, weight);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return techSignals;
|
|
328
|
+
}
|
|
329
|
+
|
|
228
330
|
/**
|
|
229
331
|
* Resolves the minimal set of skills for a given prompt, file list, and phase.
|
|
230
332
|
*
|
|
231
333
|
* @param {ResolveSkillsOptions} [options={}] - Task context options
|
|
232
334
|
* @returns {ResolvedSkillsResult} Resolved domain, role, and activated skill names
|
|
233
335
|
*/
|
|
234
|
-
function resolveSkills({ prompt = '', files = [], phase = 'Build', domain = '' } = {}) {
|
|
336
|
+
function resolveSkills({ prompt = '', files = [], phase = 'Build', domain = '', projectDir = process.cwd() } = {}) {
|
|
235
337
|
const promptText = (prompt || '').toLowerCase();
|
|
236
338
|
const scores = new Map();
|
|
237
339
|
|
|
@@ -267,6 +369,14 @@ function resolveSkills({ prompt = '', files = [], phase = 'Build', domain = '' }
|
|
|
267
369
|
}
|
|
268
370
|
}
|
|
269
371
|
|
|
372
|
+
// Phase 2: Merge AST & Project Dependency Graph Signals
|
|
373
|
+
if (projectDir) {
|
|
374
|
+
const astSignals = analyzeImportGraph(projectDir);
|
|
375
|
+
for (const [skill, astScore] of astSignals) {
|
|
376
|
+
scores.set(skill, (scores.get(skill) || 0) + astScore);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
270
380
|
// Sort matched domain skills by score descending
|
|
271
381
|
const sortedDomainSkills = Array.from(scores.entries())
|
|
272
382
|
.sort((a, b) => b[1] - a[1])
|
|
@@ -332,5 +442,6 @@ function formatDeclaration(resolution) {
|
|
|
332
442
|
module.exports = {
|
|
333
443
|
buildSkillIndex,
|
|
334
444
|
resolveSkills,
|
|
445
|
+
analyzeImportGraph,
|
|
335
446
|
formatDeclaration,
|
|
336
447
|
};
|
package/.agents/stats.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/stats.js
|
|
3
|
+
* ContextOS — Context Savings & Token Optimization Report
|
|
4
|
+
*
|
|
5
|
+
* Measures:
|
|
6
|
+
* 1. Full context payload (all skills + AGENTS.md)
|
|
7
|
+
* 2. Profiled context payload (after applying active profile exclusions)
|
|
8
|
+
* 3. Dynamically resolved context (lean on-demand skill set for typical tasks)
|
|
9
|
+
* 4. Concrete token savings percentage and cost reductions
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const profiles = require('./profiles.js');
|
|
17
|
+
|
|
18
|
+
function estimateTokens(text) {
|
|
19
|
+
// Common heuristic across GPT-4, Claude, Gemini: ~4 characters per token
|
|
20
|
+
return Math.max(1, Math.round(text.length / 4));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function calculateContextStats(projectDir = process.cwd()) {
|
|
24
|
+
const agentsDir = path.join(projectDir, '.agents');
|
|
25
|
+
const skillsDir = path.join(agentsDir, 'core', 'skills');
|
|
26
|
+
const agentsMdPath = path.join(agentsDir, 'AGENTS.md');
|
|
27
|
+
|
|
28
|
+
let agentsMdChars = 0;
|
|
29
|
+
if (fs.existsSync(agentsMdPath)) {
|
|
30
|
+
agentsMdChars = fs.readFileSync(agentsMdPath, 'utf8').length;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const skillCharMap = new Map();
|
|
34
|
+
if (fs.existsSync(skillsDir)) {
|
|
35
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (!entry.isDirectory()) continue;
|
|
38
|
+
const skillMd = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
39
|
+
if (fs.existsSync(skillMd)) {
|
|
40
|
+
skillCharMap.set(entry.name, fs.readFileSync(skillMd, 'utf8').length);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const totalSkillCount = skillCharMap.size;
|
|
46
|
+
let fullChars = agentsMdChars;
|
|
47
|
+
for (const chars of skillCharMap.values()) {
|
|
48
|
+
fullChars += chars;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Active profile
|
|
52
|
+
const activeProfile = profiles.getActiveProfile(projectDir);
|
|
53
|
+
const profileName = activeProfile ? (activeProfile.name || activeProfile.profile) : 'startup (recommended)';
|
|
54
|
+
const excludedSkills = new Set(
|
|
55
|
+
activeProfile ? activeProfile.exclude_skills || [] : (profiles.getProfile('startup')?.exclude_skills || [])
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
let profileChars = agentsMdChars;
|
|
59
|
+
let profiledSkillCount = 0;
|
|
60
|
+
for (const [skill, chars] of skillCharMap) {
|
|
61
|
+
if (!excludedSkills.has(skill)) {
|
|
62
|
+
profileChars += chars;
|
|
63
|
+
profiledSkillCount++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Resolved context: 2 foundational skills + average 2 domain skills (e.g. react + ui-ux-pro)
|
|
68
|
+
const typicalResolvedSkills = ['engineering-workflow', 'ponytail-mindset', 'react', 'ui-ux-pro'];
|
|
69
|
+
let resolvedChars = agentsMdChars;
|
|
70
|
+
let resolvedSkillCount = 0;
|
|
71
|
+
for (const s of typicalResolvedSkills) {
|
|
72
|
+
if (skillCharMap.has(s)) {
|
|
73
|
+
resolvedChars += skillCharMap.get(s);
|
|
74
|
+
resolvedSkillCount++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fullTokens = estimateTokens({ length: fullChars });
|
|
79
|
+
const profileTokens = estimateTokens({ length: profileChars });
|
|
80
|
+
const resolvedTokens = estimateTokens({ length: resolvedChars });
|
|
81
|
+
|
|
82
|
+
const profileSavings = fullTokens > 0 ? (((fullTokens - profileTokens) / fullTokens) * 100).toFixed(1) : '0.0';
|
|
83
|
+
const resolvedSavings = fullTokens > 0 ? (((fullTokens - resolvedTokens) / fullTokens) * 100).toFixed(1) : '0.0';
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
totalSkillCount,
|
|
87
|
+
profiledSkillCount,
|
|
88
|
+
resolvedSkillCount,
|
|
89
|
+
profileName,
|
|
90
|
+
full: { chars: fullChars, tokens: fullTokens },
|
|
91
|
+
profile: { chars: profileChars, tokens: profileTokens, savings: profileSavings },
|
|
92
|
+
resolved: { chars: resolvedChars, tokens: resolvedTokens, savings: resolvedSavings },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function runStats(projectDir = process.cwd()) {
|
|
97
|
+
const stats = calculateContextStats(projectDir);
|
|
98
|
+
|
|
99
|
+
console.log('\nContextOS — Context Savings Report\n');
|
|
100
|
+
console.log('┌──────────────────────────────────────┬─────────────┬──────────┐');
|
|
101
|
+
console.log('│ Mode │ Tokens │ Savings │');
|
|
102
|
+
console.log('├──────────────────────────────────────┼─────────────┼──────────┤');
|
|
103
|
+
|
|
104
|
+
const fullLabel = `Full (${stats.totalSkillCount} skills)`;
|
|
105
|
+
const fullTok = stats.full.tokens.toLocaleString().padStart(9);
|
|
106
|
+
console.log(`│ ${fullLabel.padEnd(36)} │ ${fullTok} │ — │`);
|
|
107
|
+
|
|
108
|
+
const profLabel = `Profile: ${stats.profileName} (${stats.profiledSkillCount} skills)`;
|
|
109
|
+
const profTok = stats.profile.tokens.toLocaleString().padStart(9);
|
|
110
|
+
const profSav = `${stats.profile.savings}%`.padStart(7);
|
|
111
|
+
console.log(`│ ${profLabel.slice(0, 36).padEnd(36)} │ ${profTok} │ ${profSav} │`);
|
|
112
|
+
|
|
113
|
+
const resLabel = `Resolved: typical task (${stats.resolvedSkillCount} skills)`;
|
|
114
|
+
const resTok = stats.resolved.tokens.toLocaleString().padStart(9);
|
|
115
|
+
const resSav = `${stats.resolved.savings}%`.padStart(7);
|
|
116
|
+
console.log(`│ ${resLabel.slice(0, 36).padEnd(36)} │ ${resTok} │ ${resSav} │`);
|
|
117
|
+
|
|
118
|
+
console.log('└──────────────────────────────────────┴─────────────┴──────────┘');
|
|
119
|
+
console.log('\nToken estimation: ~4 chars/token (GPT-4 / Claude / Gemini approximation)');
|
|
120
|
+
console.log('Dynamic skill resolution prevents prompt bloat and cuts LLM API costs.\n');
|
|
121
|
+
|
|
122
|
+
return stats;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (require.main === module) {
|
|
126
|
+
runStats();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
calculateContextStats,
|
|
131
|
+
runStats,
|
|
132
|
+
estimateTokens,
|
|
133
|
+
};
|