contextos-agents 1.5.0 → 1.6.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/.agents/adapters/cursor/export.js +34 -37
- package/.agents/ctx.js +6 -5
- package/.agents/doctor.js +269 -0
- package/.agents/mcp/runtime.py +17 -1
- package/.agents/mcp/server.mjs +42 -29
- package/.agents/plugins.js +21 -11
- package/.agents/resolver.js +112 -1
- package/.agents/stats.js +133 -0
- package/.agents/watch.js +161 -0
- package/README.md +46 -6
- package/bin/index.js +126 -36
- package/package.json +9 -2
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
|
+
};
|
package/.agents/watch.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/watch.js
|
|
3
|
+
* ContextOS — Continuous Context Sync & File Watcher Daemon
|
|
4
|
+
*
|
|
5
|
+
* Watches:
|
|
6
|
+
* 1. .agents/core/skills/ — regenerates agent configs on skill markdown edits
|
|
7
|
+
* 2. package.json & manifests — detects tech stack drift and recommends profile adjustments
|
|
8
|
+
*
|
|
9
|
+
* Provides instant feedback during local development without manual re-exports.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { execFileSync } = require('child_process');
|
|
17
|
+
const profiles = require('./profiles.js');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Runs the ContextOS watch daemon.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} [projectDir=process.cwd()] - Target project directory
|
|
23
|
+
* @param {Object} [options={}] - Watcher configuration options
|
|
24
|
+
* @param {number} [options.debounceMs=300] - Debounce delay in milliseconds
|
|
25
|
+
* @param {Function} [options.onSync] - Callback after a sync event completes
|
|
26
|
+
* @param {boolean} [options.exitOnSigint=true] - Whether to register process exit handlers
|
|
27
|
+
* @returns {Object} Controller object with a `close()` method
|
|
28
|
+
*/
|
|
29
|
+
function runWatch(projectDir = process.cwd(), options = {}) {
|
|
30
|
+
const debounceMs = options.debounceMs || 300;
|
|
31
|
+
const agentsDir = path.join(projectDir, '.agents');
|
|
32
|
+
const skillsDir = path.join(agentsDir, 'core', 'skills');
|
|
33
|
+
const ctxPath = path.join(agentsDir, 'ctx.js');
|
|
34
|
+
|
|
35
|
+
console.log('\n[ContextOS Watch] Initializing background continuous sync daemon...');
|
|
36
|
+
console.log(`[ContextOS Watch] Project directory: ${projectDir}`);
|
|
37
|
+
|
|
38
|
+
let lastStack = profiles.detectStack(projectDir);
|
|
39
|
+
console.log(`[ContextOS Watch] Initial stack: ${lastStack.detected.join(', ') || 'Generic JS'} (${lastStack.recommendedProfile})`);
|
|
40
|
+
|
|
41
|
+
let debounceTimer = null;
|
|
42
|
+
let isSyncing = false;
|
|
43
|
+
const watchers = [];
|
|
44
|
+
|
|
45
|
+
function triggerRecompile(sourceReason) {
|
|
46
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
47
|
+
|
|
48
|
+
debounceTimer = setTimeout(() => {
|
|
49
|
+
if (isSyncing) return;
|
|
50
|
+
isSyncing = true;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const time = new Date().toLocaleTimeString();
|
|
54
|
+
console.log(`[${time}] [SYNC] Changes detected in ${sourceReason}. Recompiling agent skills...`);
|
|
55
|
+
|
|
56
|
+
if (fs.existsSync(ctxPath)) {
|
|
57
|
+
execFileSync(process.execPath, [ctxPath, 'export', 'all'], {
|
|
58
|
+
cwd: projectDir,
|
|
59
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
60
|
+
});
|
|
61
|
+
console.log(`[${time}] [OK] Skills exported to all agents (Gemini, Claude, Cursor, Copilot, Aider, Zed).`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof options.onSync === 'function') {
|
|
65
|
+
options.onSync({ type: 'recompile', reason: sourceReason });
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(`[WARN] Auto-recompile failed: ${err.message}`);
|
|
69
|
+
} finally {
|
|
70
|
+
isSyncing = false;
|
|
71
|
+
}
|
|
72
|
+
}, debounceMs);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function checkStackChange() {
|
|
76
|
+
try {
|
|
77
|
+
const currentStack = profiles.detectStack(projectDir);
|
|
78
|
+
const prevStr = lastStack.detected.sort().join(',');
|
|
79
|
+
const currStr = currentStack.detected.sort().join(',');
|
|
80
|
+
|
|
81
|
+
if (prevStr !== currStr) {
|
|
82
|
+
const time = new Date().toLocaleTimeString();
|
|
83
|
+
console.log(`\n[${time}] [DETECT] Project dependencies changed!`);
|
|
84
|
+
console.log(` Detected stack : ${currentStack.detected.join(', ')}`);
|
|
85
|
+
console.log(` Recommendation : Profile '${currentStack.recommendedProfile}'`);
|
|
86
|
+
console.log(` Run: contextos profile apply ${currentStack.recommendedProfile}\n`);
|
|
87
|
+
lastStack = currentStack;
|
|
88
|
+
|
|
89
|
+
if (typeof options.onSync === 'function') {
|
|
90
|
+
options.onSync({ type: 'stack_change', stack: currentStack });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// ignore transient filesystem read errors
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 1. Watch .agents/core/skills/ directory
|
|
99
|
+
if (fs.existsSync(skillsDir)) {
|
|
100
|
+
try {
|
|
101
|
+
const skillsWatcher = fs.watch(skillsDir, { recursive: true }, (eventType, filename) => {
|
|
102
|
+
if (!filename) return;
|
|
103
|
+
const norm = filename.replace(/\\/g, '/');
|
|
104
|
+
if (norm.endsWith('.md') || norm.endsWith('.json') || norm.endsWith('.yaml') || norm.endsWith('.yml')) {
|
|
105
|
+
triggerRecompile(`skills/${filename}`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
watchers.push(skillsWatcher);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.warn(`[WARN] Could not establish recursive watcher on skills directory: ${e.message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 2. Watch package.json
|
|
115
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
116
|
+
if (fs.existsSync(pkgPath)) {
|
|
117
|
+
try {
|
|
118
|
+
const pkgWatcher = fs.watch(pkgPath, () => {
|
|
119
|
+
checkStackChange();
|
|
120
|
+
});
|
|
121
|
+
watchers.push(pkgWatcher);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.warn(`[WARN] Could not watch package.json: ${e.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log('[ContextOS Watch] Watching for skill updates and dependency shifts. Press Ctrl+C to stop.\n');
|
|
128
|
+
|
|
129
|
+
function close() {
|
|
130
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
131
|
+
for (const w of watchers) {
|
|
132
|
+
try {
|
|
133
|
+
w.close();
|
|
134
|
+
} catch {}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (options.exitOnSigint !== false) {
|
|
139
|
+
const onExit = () => {
|
|
140
|
+
console.log('\n[ContextOS Watch] Stopping watcher daemon.');
|
|
141
|
+
close();
|
|
142
|
+
process.exit(0);
|
|
143
|
+
};
|
|
144
|
+
process.once('SIGINT', onExit);
|
|
145
|
+
process.once('SIGTERM', onExit);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
close,
|
|
150
|
+
triggerRecompile,
|
|
151
|
+
checkStackChange,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (require.main === module) {
|
|
156
|
+
runWatch();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
runWatch,
|
|
161
|
+
};
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/contextos-agents)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://nodejs.org/)
|
|
6
6
|
[](#testing)
|
|
7
7
|
|
|
8
8
|
This is an open-source set of skills and behavioral rules for AI assistants. The package automatically installs an `.agents` folder into your project, teaching your AI assistant software development best practices (UI Design, Architecture, Security, and more).
|
|
@@ -49,10 +49,10 @@ Most AI coding assistants suffer from two extremes: they either operate in a vac
|
|
|
49
49
|
|
|
50
50
|
### Key Developer Advantages
|
|
51
51
|
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
52
|
+
- **Zero-Config Onboarding:** Run `npx contextos-agents` in your repository. It auto-detects your stack (React, Node, Python, etc.) and sets up the ideal profile in seconds.
|
|
53
|
+
- **Tailored Project Profiles:** Use `mvp` for lean, rapid prototyping without bloated microservices boilerplate, or `enterprise` for strict TDD, DDD, and security auditing.
|
|
54
|
+
- **Autonomous Multi-Agent Worktrees:** Run parallel tasks safely with the bundled MCP server—subagents work in isolated Git worktrees without corrupting your active workspace.
|
|
55
|
+
- **Verifiable Benchmarks:** Backed by reproducible side-by-side benchmarks demonstrating measurable code quality improvements and reduced token usage.
|
|
56
56
|
|
|
57
57
|
## Project Profiles & Stack Auto-Detection
|
|
58
58
|
|
|
@@ -209,6 +209,32 @@ ContextOS commits generated adapter configurations (`.cursorrules`, `.cursor/rul
|
|
|
209
209
|
- **Automated Sync & Drift Prevention:** CI strictly validates that generated exports match source skills (`node .agents/ctx.js validate`). Any uncommitted adapter drift fails CI checks via `git diff --exit-code`.
|
|
210
210
|
- **Contributor Workflow:** Source rules are authored exclusively in `.agents/core/skills/<name>/SKILL.md`. Running `node .agents/ctx.js export all` regenerates all assistant configurations deterministically.
|
|
211
211
|
|
|
212
|
+
### CI Quality Gate Action (`contextos-gate`)
|
|
213
|
+
|
|
214
|
+
You can guard your repository against skill drift, secret leaks, and rule regressions using the official reusable GitHub Composite Action:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
# .github/workflows/pr-gate.yml
|
|
218
|
+
name: ContextOS Quality Gate
|
|
219
|
+
|
|
220
|
+
on:
|
|
221
|
+
pull_request:
|
|
222
|
+
branches: [main]
|
|
223
|
+
push:
|
|
224
|
+
branches: [main]
|
|
225
|
+
|
|
226
|
+
jobs:
|
|
227
|
+
gate:
|
|
228
|
+
runs-on: ubuntu-latest
|
|
229
|
+
steps:
|
|
230
|
+
- uses: actions/checkout@v4
|
|
231
|
+
- uses: kok-o/contextos-agents/.github/actions/contextos-gate@main
|
|
232
|
+
with:
|
|
233
|
+
node-version: '20'
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The action validates skill frontmatter integrity, checks for adapter configuration drift, scans for accidental secrets or API keys, and runs your test suite.
|
|
237
|
+
|
|
212
238
|
### Plugin Skills & Validation
|
|
213
239
|
|
|
214
240
|
You can expand your `.agents` folder with community plugins or validate your own custom skills using the top-level commands:
|
|
@@ -381,7 +407,7 @@ The runtime sandbox evaluates model outputs against real-world engineering invar
|
|
|
381
407
|
| **DDD Order Aggregate Root** | Architecture & DDD | `ddd`, `system-design`, `decisions` | Immutable `Money` Value Object, state-machine invariants (PENDING → PAID → SHIPPED), explicit Domain Event classes with queue draining. |
|
|
382
408
|
| **Resilient API Client** | Reliability & Async | `typescript`, `system-design`, `performance` | 3-state Circuit Breaker (CLOSED → OPEN → HALF-OPEN), `AbortController` timeouts, typed error taxonomy without credential leakage. |
|
|
383
409
|
|
|
384
|
-
### Running Benchmarks Locally
|
|
410
|
+
### Running Benchmarks Locally & in CI
|
|
385
411
|
|
|
386
412
|
You can run the benchmark suite locally with your own API keys:
|
|
387
413
|
|
|
@@ -393,10 +419,24 @@ npm run benchmark:runtime -- --provider gemini --model gemini-2.5-flash
|
|
|
393
419
|
# Run with OpenAI:
|
|
394
420
|
$env:OPENAI_API_KEY = "sk-..."
|
|
395
421
|
npm run benchmark:runtime -- --provider openai --model gpt-4o
|
|
422
|
+
|
|
423
|
+
# Run complete multi-model runtime matrix (Gemini, OpenRouter, AgentRouter):
|
|
424
|
+
GEMINI_API_KEY=... OPENROUTER_API_KEY=... AGENTROUTER_API_KEY=... node benchmarks/run-multi-runtime.cjs
|
|
396
425
|
```
|
|
397
426
|
|
|
398
427
|
When executed, reports are generated in `benchmarks/results/` (`.html`, `.md`, `.json`) and tracked so results are visible and shareable.
|
|
399
428
|
|
|
429
|
+
> [!NOTE]
|
|
430
|
+
> **Why Runtime Benchmarks Are Dispatch-Only in CI:** Standard CI checks (`validate-skills.yml`) run hermetically without external API calls to avoid flaky network dependencies and API token expenditures on every pull request. Live runtime evaluation is triggered on demand via GitHub Actions **Workflow Dispatch** ([`benchmark-runtime.yml`](.github/workflows/benchmark-runtime.yml)) using secure repository secrets.
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
## Security — Third-Party Skills
|
|
434
|
+
|
|
435
|
+
ContextOS skills are **executable context** — they become part of the system prompt that controls your AI agent's behavior. A malicious skill could instruct the AI agent to exfiltrate environment variables, modify files, or ignore your project's security policies.
|
|
436
|
+
|
|
437
|
+
> [!CAUTION]
|
|
438
|
+
> **Install skills only from repositories you trust as you would trust executable code.** Skills installed via `ctx.js skill add` from npm or GitHub are not sandboxed. ContextOS includes a built-in prompt injection scanner, but it cannot guarantee safety of arbitrary third-party content.
|
|
439
|
+
|
|
400
440
|
## Contributing
|
|
401
441
|
|
|
402
442
|
We are open to pull requests! See [CONTRIBUTING.md](./CONTRIBUTING.md) for a step-by-step guide on how to add a new skill.
|