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.
@@ -22,42 +22,36 @@ function getSkillGlobsAndFlags(skillName) {
22
22
  return { globs: '', alwaysApply: true };
23
23
  }
24
24
 
25
- const FRONTEND_SKILLS = [
26
- 'react', 'nextjs', 'typescript', 'ui-ux-pro', 'impeccable-design',
27
- 'ui-design', 'ux-design', 'web-accessibility', 'brutalist-design',
28
- 'minimalist-design', 'soft-design', 'redesign-audit', 'react-best-practices',
29
- ];
30
- if (FRONTEND_SKILLS.includes(skillName)) {
31
- return { globs: '**/*.{ts,tsx,js,jsx,css,scss,html}', alwaysApply: false };
32
- }
33
-
34
- const BACKEND_SKILLS = ['system-design', 'node', 'nestjs', 'microservices', 'ddd'];
35
- if (BACKEND_SKILLS.includes(skillName)) {
36
- return { globs: '**/*.{ts,js,json,prisma,sql}', alwaysApply: false };
37
- }
38
-
39
- if (skillName === 'database') {
40
- return { globs: '**/*.{prisma,sql,sqlite,db,drizzle.config.*}', alwaysApply: false };
41
- }
42
-
43
- if (skillName === 'docker') {
44
- return { globs: '**/Dockerfile*,**/docker-compose*.{yml,yaml}', alwaysApply: false };
45
- }
46
-
47
- if (skillName === 'testing') {
48
- return { globs: '**/*.{test,spec}.{ts,js,tsx,jsx},vitest.config.*,jest.config.*', alwaysApply: false };
49
- }
50
-
51
- if (skillName === 'state-management') {
52
- return { globs: '**/*{store,state,slice,reducer,atom}*.{ts,js,tsx,jsx}', alwaysApply: false };
53
- }
54
-
55
- if (skillName === 'performance' || skillName === 'vercel-optimize') {
56
- return { globs: '**/*.{ts,tsx,js,jsx,json}', alwaysApply: false };
57
- }
58
-
59
- if (skillName === 'fastapi') {
60
- return { globs: '**/*.{py,requirements.txt,Pipfile,pyproject.toml}', alwaysApply: false };
25
+ // Precise surgical globs mapping to prevent context flooding
26
+ const GLOB_MAP = {
27
+ 'react': '**/*.{tsx,jsx}',
28
+ 'react-best-practices': '**/*.{tsx,jsx}',
29
+ 'nextjs': 'app/**/*,pages/**/*,next.config.*',
30
+ 'typescript': '**/*.{ts,tsx}',
31
+ 'ui-design': '**/*.{tsx,jsx,css,scss}',
32
+ 'ui-ux-pro': '**/*.{tsx,jsx,css,scss}',
33
+ 'ux-design': '**/*.{tsx,jsx}',
34
+ 'impeccable-design': '**/*.{tsx,jsx,css,scss}',
35
+ 'brutalist-design': '**/*.{tsx,jsx,css,scss}',
36
+ 'minimalist-design': '**/*.{tsx,jsx,css,scss}',
37
+ 'soft-design': '**/*.{tsx,jsx,css,scss}',
38
+ 'redesign-audit': '**/*.{tsx,jsx,css,scss}',
39
+ 'web-accessibility': '**/*.{tsx,jsx,html}',
40
+ 'ddd': '**/domain/**/*,**/entities/**/*,**/aggregates/**/*',
41
+ 'microservices': '**/services/**/*,docker-compose.*,**/gateway/**/*',
42
+ 'nestjs': '**/*.module.ts,**/*.controller.ts,**/*.service.ts,nest-cli.json',
43
+ 'node': '**/*.{ts,js}',
44
+ 'database': '**/*.{prisma,sql,sqlite,db,drizzle.config.*}',
45
+ 'docker': '**/Dockerfile*,**/docker-compose*.{yml,yaml}',
46
+ 'testing': '**/*.{test,spec}.{ts,js,tsx,jsx},vitest.config.*,jest.config.*',
47
+ 'state-management': '**/*{store,state,slice,reducer,atom}*.{ts,js,tsx,jsx}',
48
+ 'performance': '**/*.{ts,tsx,js,jsx,json}',
49
+ 'vercel-optimize': '**/*.{ts,tsx,js,jsx,json}',
50
+ 'fastapi': '**/*.{py,requirements.txt,Pipfile,pyproject.toml}',
51
+ };
52
+
53
+ if (GLOB_MAP[skillName]) {
54
+ return { globs: GLOB_MAP[skillName], alwaysApply: false };
61
55
  }
62
56
 
63
57
  return { globs: '', alwaysApply: false };
@@ -118,7 +112,10 @@ function generateCursorMdc(skillDir) {
118
112
  const body = stripFrontmatter(raw);
119
113
  const { globs, alwaysApply } = getSkillGlobsAndFlags(skillName);
120
114
 
121
- const cleanDescription = description.replace(/\r?\n+/g, ' ').replace(/"/g, "'").trim();
115
+ let cleanDescription = description.replace(/\r?\n+/g, ' ').replace(/"/g, "'").trim();
116
+ if (!globs && !alwaysApply) {
117
+ cleanDescription = `Agent-requested: invoke when working on ${title}. ${cleanDescription}`.trim();
118
+ }
122
119
  const mdcContent = `---
123
120
  description: "${cleanDescription}"
124
121
  globs: ${globs ? `"${globs}"` : '""'}
@@ -25,6 +25,7 @@ prefer_skills:
25
25
  exclude_skills:
26
26
  - ddd
27
27
  - microservices
28
+ - system-design
28
29
  - kubernetes
29
30
  - monitoring
30
31
  - cicd
package/.agents/ctx.js CHANGED
@@ -46,6 +46,10 @@ function printHelp() {
46
46
  console.log(' audit Alias for validate (check skills)');
47
47
  console.log(' validate Validate skill sources, frontmatter, deps & sync');
48
48
  console.log(' clean-worktrees Clean up lingering .swarm-worktrees and swarm/* branches');
49
+ console.log(' doctor Run project diagnostic health check');
50
+ console.log(' stats Display token context savings report');
51
+ console.log(' watch Start continuous file watcher and auto-sync daemon');
52
+ console.log(' init Show initialization guide');
49
53
  console.log(' install-skill <ref> Alias for skill add (install a plugin)');
50
54
  console.log(' skill add <ref> Install a plugin skill (GitHub or npm)');
51
55
  console.log(' skill remove <name> Uninstall a plugin skill');
@@ -301,7 +305,8 @@ if (command === 'export') {
301
305
  }
302
306
 
303
307
  if (subcommand === 'add') {
304
- plugins.add(ref, { dryRun, checksum }).catch(err => {
308
+ const forceUnsafe = args.includes('--force-unsafe-prompts') || args.includes('--force-unsafe');
309
+ plugins.add(ref, { dryRun, checksum, forceUnsafe }).catch(err => {
305
310
  console.error(`[ERROR] ${err.message}`);
306
311
  process.exit(1);
307
312
  });
@@ -323,7 +328,7 @@ if (command === 'export') {
323
328
 
324
329
  // ── clean-worktrees ──────────────────────────────────────────────────────────
325
330
  } else if (command === 'clean-worktrees' || command === 'clean') {
326
- const { execSync } = require('child_process');
331
+ const { execSync, execFileSync } = require('child_process');
327
332
  const fs = require('fs');
328
333
  const path = require('path');
329
334
  const rootDir = path.resolve(__dirname, '..');
@@ -331,16 +336,16 @@ if (command === 'export') {
331
336
 
332
337
  console.log('Cleaning up ContextOS swarm worktrees and branches...');
333
338
  try {
334
- execSync('git worktree prune', { cwd: rootDir, stdio: 'pipe' });
339
+ execFileSync('git', ['worktree', 'prune'], { cwd: rootDir, stdio: 'pipe' });
335
340
  } catch {}
336
341
 
337
342
  let branchCount = 0;
338
343
  try {
339
- const branches = execSync('git branch --list "swarm/*"', { cwd: rootDir, encoding: 'utf-8' });
344
+ const branches = execFileSync('git', ['branch', '--list', 'swarm/*'], { cwd: rootDir, encoding: 'utf-8' });
340
345
  const list = branches.split('\n').map(b => b.replace(/^[*+\s]+/, '').trim()).filter(Boolean);
341
346
  for (const b of list) {
342
347
  try {
343
- execSync(`git branch -D "${b}"`, { cwd: rootDir, stdio: 'pipe' });
348
+ execFileSync('git', ['branch', '-D', b], { cwd: rootDir, stdio: 'pipe' });
344
349
  branchCount++;
345
350
  } catch {}
346
351
  }
@@ -362,6 +367,33 @@ if (command === 'export') {
362
367
 
363
368
  console.log(`✓ Cleaned up ${dirCount} worktree directory(ies) and ${branchCount} swarm branch(es).`);
364
369
 
370
+ // ── doctor ────────────────────────────────────────────────────────────────────
371
+ } else if (command === 'doctor') {
372
+ const doctorModule = require('./doctor.js');
373
+ doctorModule.runDoctor(process.cwd());
374
+
375
+ // ── stats ─────────────────────────────────────────────────────────────────────
376
+ } else if (command === 'stats') {
377
+ const statsModule = require('./stats.js');
378
+ statsModule.runStats(process.cwd());
379
+
380
+ // ── watch ─────────────────────────────────────────────────────────────────────
381
+ } else if (command === 'watch') {
382
+ const watchModule = require('./watch.js');
383
+ watchModule.runWatch(process.cwd());
384
+
385
+ // ── init ──────────────────────────────────────────────────────────────────────
386
+ } else if (command === 'init') {
387
+ console.log('\nContextOS — Project Initialization Guide\n');
388
+ console.log('To set up ContextOS in your project:');
389
+ console.log(' npx contextos Install .agents/ with auto-detected profile');
390
+ console.log(' npx contextos --minimal Install with minimal core skills');
391
+ console.log(' npx contextos --profile <name> Install with specific profile (mvp, startup, enterprise, etc.)');
392
+ console.log(' npx contextos --with-mcp Install with MCP execution server enabled');
393
+ console.log('\nAfter setup, verify your installation:');
394
+ console.log(' node .agents/ctx.js doctor');
395
+ console.log(' node .agents/ctx.js stats\n');
396
+
365
397
  // ── unknown ───────────────────────────────────────────────────────────────────
366
398
  } else {
367
399
  console.error(`Unknown command: ${command}`);
@@ -0,0 +1,269 @@
1
+ /**
2
+ * .agents/doctor.js
3
+ * ContextOS — Project Diagnostics & Health Check Engine
4
+ *
5
+ * Inspects:
6
+ * 1. .agents/ directory and core runtime files
7
+ * 2. Node.js environment compatibility
8
+ * 3. Active profile and skill exclusion/preference state
9
+ * 4. Compiled AI agent adapters (Gemini, Claude, Cursor, Copilot, etc.)
10
+ * 5. MCP server status and worktree support
11
+ * 6. Security controls (pre-commit hooks, secret scanning)
12
+ * 7. Project tech stack detection and recommended profile alignment
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { execFileSync } = require('child_process');
20
+ const profiles = require('./profiles.js');
21
+
22
+ const DOMAIN_MAP = {
23
+ frontend: new Set([
24
+ 'react', 'react-best-practices', 'nextjs', 'typescript',
25
+ 'ui-ux-pro', 'ui-design', 'ux-design', 'web-accessibility',
26
+ 'brutalist-design', 'minimalist-design', 'soft-design', 'redesign-audit',
27
+ 'impeccable-design', 'state-management',
28
+ ]),
29
+ backend: new Set([
30
+ 'node', 'fastapi', 'nestjs', 'database', 'system-design',
31
+ 'ddd', 'microservices',
32
+ ]),
33
+ cross: new Set([
34
+ 'engineering-workflow', 'gstack-roles', 'ponytail-mindset',
35
+ 'gemini-precision', 'interview-me', 'security', 'testing',
36
+ 'performance', 'vercel-optimize', 'docker', 'decisions',
37
+ 'architecture-diagrams', 'subagent-orchestrator', 'graphify',
38
+ 'context-manager', 'context-os', 'adapters', 'generators',
39
+ ]),
40
+ };
41
+
42
+ function checkNodeVersion() {
43
+ const version = process.version; // e.g. v22.12.0
44
+ const match = version.match(/^v?(\d+)\.(\d+)/);
45
+ if (!match) return { ok: false, version, message: `${version} (unknown)` };
46
+
47
+ const major = parseInt(match[1], 10);
48
+ const minor = parseInt(match[2], 10);
49
+ // Requires at least Node 18.0.0 or 16.7.0 for recursive cpSync
50
+ const ok = major > 16 || (major === 16 && minor >= 7);
51
+ return {
52
+ ok,
53
+ version,
54
+ message: ok ? `Node.js ${version} (>= 16.7.0 required)` : `Node.js ${version} (< 16.7.0, please upgrade)`,
55
+ };
56
+ }
57
+
58
+ function checkPreCommitHook(projectDir) {
59
+ const hookPath = path.join(projectDir, '.git', 'hooks', 'pre-commit');
60
+ if (!fs.existsSync(hookPath)) {
61
+ return { ok: false, message: 'not installed (run: npm run setup:hooks)' };
62
+ }
63
+ try {
64
+ const content = fs.readFileSync(hookPath, 'utf8');
65
+ if (content.includes('check-secrets')) {
66
+ return { ok: true, message: 'installed (with secret check)' };
67
+ }
68
+ return { ok: false, message: 'installed but missing check-secrets' };
69
+ } catch {
70
+ return { ok: false, message: 'unreadable' };
71
+ }
72
+ }
73
+
74
+ function checkSecretScanner(projectDir) {
75
+ const scriptPath = path.join(projectDir, 'scripts', 'check-secrets.js');
76
+ if (!fs.existsSync(scriptPath)) {
77
+ return { ok: true, message: 'scanner script not present (skipped)' };
78
+ }
79
+ try {
80
+ execFileSync(process.execPath, [scriptPath, '--all'], {
81
+ cwd: projectDir,
82
+ stdio: ['pipe', 'pipe', 'pipe'],
83
+ });
84
+ return { ok: true, message: '0 findings' };
85
+ } catch (err) {
86
+ return { ok: false, message: 'potential secrets detected (run: npm run check:secrets)' };
87
+ }
88
+ }
89
+
90
+ function checkCompiledAdapters(projectDir) {
91
+ const compiled = [];
92
+ const genGemini = path.join(projectDir, '.agents', 'generated', 'gemini', 'skills');
93
+ if (fs.existsSync(genGemini) && fs.readdirSync(genGemini).length > 0) {
94
+ compiled.push('gemini');
95
+ }
96
+ const genClaude = path.join(projectDir, '.agents', 'generated', 'claude', 'skills');
97
+ if (fs.existsSync(genClaude) && fs.readdirSync(genClaude).length > 0) {
98
+ compiled.push('claude');
99
+ }
100
+ if (fs.existsSync(path.join(projectDir, '.cursorrules')) || fs.existsSync(path.join(projectDir, '.cursor', 'rules'))) {
101
+ compiled.push('cursor');
102
+ }
103
+ if (fs.existsSync(path.join(projectDir, '.github', 'copilot-instructions.md'))) {
104
+ compiled.push('copilot');
105
+ }
106
+ if (fs.existsSync(path.join(projectDir, '.aider.conf.yml'))) {
107
+ compiled.push('aider');
108
+ }
109
+ if (fs.existsSync(path.join(projectDir, '.zed', 'rules.md'))) {
110
+ compiled.push('zed');
111
+ }
112
+ return compiled;
113
+ }
114
+
115
+ function inspectSkills(projectDir, activeProfile) {
116
+ const skillsDir = path.join(projectDir, '.agents', 'core', 'skills');
117
+ if (!fs.existsSync(skillsDir)) return { total: 0, active: 0, excluded: 0, byDomain: { frontend: [], backend: [], cross: [] } };
118
+
119
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
120
+ const allSkills = entries
121
+ .filter(e => e.isDirectory() && fs.existsSync(path.join(skillsDir, e.name, 'SKILL.md')))
122
+ .map(e => e.name);
123
+
124
+ const excludedSet = new Set(activeProfile ? activeProfile.exclude_skills || [] : []);
125
+ const activeSkills = allSkills.filter(s => !excludedSet.has(s));
126
+
127
+ const byDomain = { frontend: [], backend: [], cross: [] };
128
+ for (const s of activeSkills) {
129
+ if (DOMAIN_MAP.frontend.has(s)) byDomain.frontend.push(s);
130
+ else if (DOMAIN_MAP.backend.has(s)) byDomain.backend.push(s);
131
+ else byDomain.cross.push(s);
132
+ }
133
+
134
+ return {
135
+ total: allSkills.length,
136
+ active: activeSkills.length,
137
+ excluded: excludedSet.size,
138
+ byDomain,
139
+ };
140
+ }
141
+
142
+ function runDoctor(projectDir = process.cwd()) {
143
+ const version = (() => {
144
+ try {
145
+ return require('../package.json').version;
146
+ } catch {
147
+ return '1.5.0';
148
+ }
149
+ })();
150
+
151
+ const agentsDir = path.join(projectDir, '.agents');
152
+ const hasAgents = fs.existsSync(agentsDir);
153
+ const nodeCheck = checkNodeVersion();
154
+ const activeProfile = hasAgents ? profiles.getActiveProfile(projectDir) : null;
155
+ const skillsInfo = inspectSkills(projectDir, activeProfile);
156
+ const compiledAdapters = checkCompiledAdapters(projectDir);
157
+
158
+ const mcpServerPath = path.join(projectDir, '.agents', 'mcp', 'server.mjs');
159
+ const hasMcp = fs.existsSync(mcpServerPath);
160
+
161
+ const hookCheck = checkPreCommitHook(projectDir);
162
+ const secretCheck = checkSecretScanner(projectDir);
163
+ const stack = profiles.detectStack(projectDir);
164
+
165
+ console.log('\n┌─────────────────────────────────────────────────────────────┐');
166
+ console.log(`│ ContextOS Doctor — Project Health Check v${version.padEnd(16)}│`);
167
+ console.log('├─────────────────────────────────────────────────────────────┤');
168
+ console.log('│ │');
169
+
170
+ // .agents directory
171
+ if (hasAgents) {
172
+ console.log('│ ✓ .agents/ directory found │');
173
+ } else {
174
+ console.log('│ ✗ .agents/ directory missing (run: npx contextos-agents) │');
175
+ }
176
+
177
+ // Node version
178
+ const nodeIcon = nodeCheck.ok ? '✓' : '✗';
179
+ console.log(`│ ${nodeIcon} ${nodeCheck.message.padEnd(58)}│`);
180
+
181
+ // Active profile
182
+ if (activeProfile) {
183
+ const profStr = `Active profile: ${activeProfile.name || activeProfile.profile}`;
184
+ console.log(`│ ✓ ${profStr.padEnd(58)}│`);
185
+ } else {
186
+ console.log('│ • Active profile: default (no profile locked) │');
187
+ }
188
+
189
+ // Skills
190
+ const skillsStr = `Skills loaded: ${skillsInfo.active} / ${skillsInfo.total}` +
191
+ (skillsInfo.excluded > 0 ? ` (${skillsInfo.excluded} excluded by profile)` : '');
192
+ console.log(`│ ✓ ${skillsStr.padEnd(58)}│`);
193
+
194
+ // Adapters
195
+ if (compiledAdapters.length > 0) {
196
+ const adaptStr = `Adapters compiled: ${compiledAdapters.join(', ')}`;
197
+ console.log(`│ ✓ ${adaptStr.padEnd(58)}│`);
198
+ } else {
199
+ console.log('│ • Adapters compiled: none (run: contextos export gemini) │');
200
+ }
201
+
202
+ // MCP
203
+ if (hasMcp) {
204
+ console.log('│ ✓ MCP server: installed (.agents/mcp/server.mjs) │');
205
+ } else {
206
+ console.log('│ • MCP server: not installed (run: contextos setup-mcp) │');
207
+ }
208
+
209
+ // Security
210
+ const hookIcon = hookCheck.ok ? '✓' : '•';
211
+ const hookStr = `Pre-commit hook: ${hookCheck.message}`;
212
+ console.log(`│ ${hookIcon} ${hookStr.padEnd(58)}│`);
213
+
214
+ const secretIcon = secretCheck.ok ? '✓' : '✗';
215
+ const secretStr = `Secret scanner: ${secretCheck.message}`;
216
+ console.log(`│ ${secretIcon} ${secretStr.padEnd(58)}│`);
217
+
218
+ console.log('│ │');
219
+ console.log('├─────────────────────────────────────────────────────────────┤');
220
+ console.log('│ Project Stack & Profile Recommendations │');
221
+ console.log('├─────────────────────────────────────────────────────────────┤');
222
+ console.log('│ │');
223
+
224
+ const detectedStr = `Detected Stack: ${stack.detected.length ? stack.detected.join(', ') : 'Generic JS'}`;
225
+ console.log(`│ • ${detectedStr.slice(0, 56).padEnd(56)} │`);
226
+
227
+ const recProfStr = `Recommended Profile: ${stack.recommendedProfile}`;
228
+ console.log(`│ • ${recProfStr.padEnd(56)} │`);
229
+
230
+ console.log('│ │');
231
+ console.log('│ Skills by Domain: │');
232
+
233
+ const feStr = `Frontend (${skillsInfo.byDomain.frontend.length}): ${skillsInfo.byDomain.frontend.slice(0, 4).join(', ')}${skillsInfo.byDomain.frontend.length > 4 ? '...' : ''}`;
234
+ console.log(`│ ${feStr.padEnd(57)}│`);
235
+
236
+ const beStr = `Backend (${skillsInfo.byDomain.backend.length}): ${skillsInfo.byDomain.backend.slice(0, 4).join(', ')}${skillsInfo.byDomain.backend.length > 4 ? '...' : ''}`;
237
+ console.log(`│ ${beStr.padEnd(57)}│`);
238
+
239
+ const crStr = `Cross (${skillsInfo.byDomain.cross.length}): ${skillsInfo.byDomain.cross.slice(0, 4).join(', ')}${skillsInfo.byDomain.cross.length > 4 ? '...' : ''}`;
240
+ console.log(`│ ${crStr.padEnd(57)}│`);
241
+
242
+ console.log('│ │');
243
+ console.log('└─────────────────────────────────────────────────────────────┘\n');
244
+
245
+ return {
246
+ hasAgents,
247
+ nodeOk: nodeCheck.ok,
248
+ activeProfile,
249
+ skillsInfo,
250
+ compiledAdapters,
251
+ hasMcp,
252
+ hookCheck,
253
+ secretCheck,
254
+ stack,
255
+ };
256
+ }
257
+
258
+ if (require.main === module) {
259
+ runDoctor();
260
+ }
261
+
262
+ module.exports = {
263
+ runDoctor,
264
+ checkNodeVersion,
265
+ checkPreCommitHook,
266
+ checkSecretScanner,
267
+ checkCompiledAdapters,
268
+ inspectSkills,
269
+ };
@@ -38,6 +38,8 @@ _FORBIDDEN_ATTRIBUTES = {
38
38
  '__globals__', '__code__', '__builtins__', '__import__',
39
39
  '__getattr__', '__setattr__', '__delattr__',
40
40
  '__reduce__', '__reduce_ex__', # pickle-based escapes
41
+ 'create_subprocess_shell', 'create_subprocess_exec',
42
+ 'open_connection', 'start_server', 'connect_read_pipe', 'connect_write_pipe',
41
43
  }
42
44
 
43
45
  def _safe_import(name, globals=None, locals=None, fromlist=(), level=0):
@@ -340,6 +342,20 @@ def merge_threads() -> str:
340
342
  return raw
341
343
 
342
344
 
345
+ class _SafeAsyncio:
346
+ gather = staticmethod(asyncio.gather)
347
+ sleep = staticmethod(asyncio.sleep)
348
+ wait = staticmethod(asyncio.wait)
349
+ Event = asyncio.Event
350
+ Queue = asyncio.Queue
351
+ Lock = asyncio.Lock
352
+ Semaphore = asyncio.Semaphore
353
+ TimeoutError = asyncio.TimeoutError
354
+ as_completed = staticmethod(asyncio.as_completed)
355
+
356
+ _safe_asyncio = _SafeAsyncio()
357
+
358
+
343
359
  def _runtime_symbols() -> dict:
344
360
  """Return the dict of runtime symbols injected into the user namespace."""
345
361
  return {
@@ -352,7 +368,7 @@ def _runtime_symbols() -> dict:
352
368
  'merge_threads': merge_threads,
353
369
  'FINAL': FINAL,
354
370
  'FINAL_VAR': FINAL_VAR,
355
- 'asyncio': asyncio,
371
+ 'asyncio': _safe_asyncio,
356
372
  'json': json,
357
373
  }
358
374