prjct-cli 0.9.2 → 0.10.2
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 +142 -0
- package/core/__tests__/agentic/memory-system.test.js +263 -0
- package/core/__tests__/agentic/plan-mode.test.js +336 -0
- package/core/agentic/agent-router.js +253 -186
- package/core/agentic/chain-of-thought.js +578 -0
- package/core/agentic/command-executor.js +299 -17
- package/core/agentic/context-builder.js +208 -8
- package/core/agentic/context-filter.js +83 -83
- package/core/agentic/ground-truth.js +591 -0
- package/core/agentic/loop-detector.js +406 -0
- package/core/agentic/memory-system.js +850 -0
- package/core/agentic/parallel-tools.js +366 -0
- package/core/agentic/plan-mode.js +572 -0
- package/core/agentic/prompt-builder.js +127 -2
- package/core/agentic/response-templates.js +290 -0
- package/core/agentic/semantic-compression.js +517 -0
- package/core/agentic/think-blocks.js +657 -0
- package/core/agentic/tool-registry.js +32 -0
- package/core/agentic/validation-rules.js +380 -0
- package/core/command-registry.js +48 -0
- package/core/commands.js +128 -60
- package/core/context-sync.js +183 -0
- package/core/domain/agent-generator.js +77 -46
- package/core/domain/agent-loader.js +183 -0
- package/core/domain/agent-matcher.js +217 -0
- package/core/domain/agent-validator.js +217 -0
- package/core/domain/context-estimator.js +175 -0
- package/core/domain/product-standards.js +92 -0
- package/core/domain/smart-cache.js +157 -0
- package/core/domain/task-analyzer.js +353 -0
- package/core/domain/tech-detector.js +365 -0
- package/package.json +8 -16
- package/templates/commands/done.md +7 -0
- package/templates/commands/feature.md +8 -0
- package/templates/commands/ship.md +8 -0
- package/templates/commands/spec.md +128 -0
- package/templates/global/CLAUDE.md +17 -0
- package/core/__tests__/agentic/agent-router.test.js +0 -398
- package/core/__tests__/agentic/command-executor.test.js +0 -223
- package/core/__tests__/agentic/context-builder.test.js +0 -160
- package/core/__tests__/agentic/context-filter.test.js +0 -494
- package/core/__tests__/agentic/prompt-builder.test.js +0 -212
- package/core/__tests__/agentic/template-loader.test.js +0 -164
- package/core/__tests__/agentic/tool-registry.test.js +0 -243
- package/core/__tests__/domain/agent-generator.test.js +0 -296
- package/core/__tests__/domain/analyzer.test.js +0 -324
- package/core/__tests__/infrastructure/author-detector.test.js +0 -103
- package/core/__tests__/infrastructure/config-manager.test.js +0 -454
- package/core/__tests__/infrastructure/path-manager.test.js +0 -412
- package/core/__tests__/setup.test.js +0 -15
- package/core/__tests__/utils/date-helper.test.js +0 -169
- package/core/__tests__/utils/file-helper.test.js +0 -258
- package/core/__tests__/utils/jsonl-helper.test.js +0 -387
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextEstimator - Pre-filter files before building context
|
|
3
|
+
*
|
|
4
|
+
* Estimates which files are needed based on task analysis
|
|
5
|
+
* BEFORE building full context - saves I/O
|
|
6
|
+
*
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path')
|
|
11
|
+
const { glob } = require('glob')
|
|
12
|
+
|
|
13
|
+
class ContextEstimator {
|
|
14
|
+
/**
|
|
15
|
+
* Estimate which files are needed for a task
|
|
16
|
+
* @param {Object} taskAnalysis - Task analysis result
|
|
17
|
+
* @param {string} projectPath - Project path
|
|
18
|
+
* @returns {Promise<Array<string>>} Estimated file paths
|
|
19
|
+
*/
|
|
20
|
+
async estimateFiles(taskAnalysis, projectPath) {
|
|
21
|
+
const domain = taskAnalysis.primaryDomain
|
|
22
|
+
const projectTech = taskAnalysis.projectTechnologies || {}
|
|
23
|
+
const semantic = taskAnalysis.semantic || {}
|
|
24
|
+
|
|
25
|
+
// Get patterns for this domain
|
|
26
|
+
const patterns = this.getPatternsForDomain(domain, projectTech)
|
|
27
|
+
|
|
28
|
+
// Expand with semantic understanding
|
|
29
|
+
if (semantic.requiresMultipleAgents && semantic.agents) {
|
|
30
|
+
// Multi-agent task - combine patterns
|
|
31
|
+
const allPatterns = semantic.agents.reduce((acc, agentDomain) => {
|
|
32
|
+
const agentPatterns = this.getPatternsForDomain(agentDomain, projectTech)
|
|
33
|
+
return {
|
|
34
|
+
include: [...acc.include, ...agentPatterns.include],
|
|
35
|
+
extensions: [...acc.extensions, ...agentPatterns.extensions]
|
|
36
|
+
}
|
|
37
|
+
}, { include: [], extensions: [] })
|
|
38
|
+
|
|
39
|
+
patterns.include = [...new Set(allPatterns.include)]
|
|
40
|
+
patterns.extensions = [...new Set(allPatterns.extensions)]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Find files matching patterns
|
|
44
|
+
const files = await this.findMatchingFiles(projectPath, patterns)
|
|
45
|
+
|
|
46
|
+
// Limit to reasonable number
|
|
47
|
+
const maxFiles = 200
|
|
48
|
+
return files.slice(0, maxFiles)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get file patterns for a domain
|
|
53
|
+
*/
|
|
54
|
+
getPatternsForDomain(domain, projectTech) {
|
|
55
|
+
const patterns = {
|
|
56
|
+
include: [],
|
|
57
|
+
extensions: [],
|
|
58
|
+
exclude: ['node_modules', 'dist', 'build', '.git']
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
switch (domain) {
|
|
62
|
+
case 'frontend':
|
|
63
|
+
patterns.include = ['src', 'components', 'pages', 'views', 'app']
|
|
64
|
+
patterns.extensions = ['.tsx', '.jsx', '.vue', '.svelte', '.css', '.scss', '.styled.js']
|
|
65
|
+
|
|
66
|
+
// Add framework-specific patterns
|
|
67
|
+
if (projectTech.frameworks) {
|
|
68
|
+
if (projectTech.frameworks.some(f => f.toLowerCase().includes('next'))) {
|
|
69
|
+
patterns.include.push('pages', 'app', 'components')
|
|
70
|
+
}
|
|
71
|
+
if (projectTech.frameworks.some(f => f.toLowerCase().includes('react'))) {
|
|
72
|
+
patterns.extensions.push('.tsx', '.jsx')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
case 'backend':
|
|
78
|
+
patterns.include = ['src', 'lib', 'api', 'routes', 'controllers', 'services', 'app']
|
|
79
|
+
patterns.extensions = ['.js', '.ts', '.py', '.rb', '.go', '.rs']
|
|
80
|
+
|
|
81
|
+
// Framework-specific
|
|
82
|
+
if (projectTech.frameworks) {
|
|
83
|
+
if (projectTech.frameworks.some(f => f.toLowerCase().includes('express'))) {
|
|
84
|
+
patterns.include.push('routes', 'middleware', 'controllers')
|
|
85
|
+
}
|
|
86
|
+
if (projectTech.frameworks.some(f => f.toLowerCase().includes('django'))) {
|
|
87
|
+
patterns.include.push('views', 'urls', 'models')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
case 'database':
|
|
93
|
+
patterns.include = ['migrations', 'models', 'schemas', 'db', 'database']
|
|
94
|
+
patterns.extensions = ['.sql', '.js', '.ts', '.rb', '.py']
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
case 'qa':
|
|
98
|
+
patterns.include = ['tests', '__tests__', 'spec', 'test']
|
|
99
|
+
patterns.extensions = ['.test.js', '.test.ts', '.spec.js', '.spec.ts']
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
case 'devops':
|
|
103
|
+
patterns.include = ['.github', '.gitlab', 'docker', 'k8s', 'kubernetes', 'terraform']
|
|
104
|
+
patterns.extensions = ['.yml', '.yaml', '.dockerfile', '.sh', '.tf']
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
default:
|
|
108
|
+
// General - include common source directories
|
|
109
|
+
patterns.include = ['src', 'lib', 'app']
|
|
110
|
+
patterns.extensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb']
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return patterns
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Find files matching patterns
|
|
118
|
+
*/
|
|
119
|
+
async findMatchingFiles(projectPath, patterns) {
|
|
120
|
+
const files = []
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// Build glob patterns
|
|
124
|
+
const globPatterns = []
|
|
125
|
+
|
|
126
|
+
// Add include patterns
|
|
127
|
+
for (const include of patterns.include) {
|
|
128
|
+
for (const ext of patterns.extensions) {
|
|
129
|
+
globPatterns.push(`${include}/**/*${ext}`)
|
|
130
|
+
globPatterns.push(`**/${include}/**/*${ext}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Also search root level
|
|
135
|
+
for (const ext of patterns.extensions) {
|
|
136
|
+
globPatterns.push(`*${ext}`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Execute glob searches
|
|
140
|
+
for (const pattern of globPatterns) {
|
|
141
|
+
try {
|
|
142
|
+
const matches = await glob(pattern, {
|
|
143
|
+
cwd: projectPath,
|
|
144
|
+
ignore: patterns.exclude.map(ex => `**/${ex}/**`),
|
|
145
|
+
nodir: true,
|
|
146
|
+
follow: false
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
if (Array.isArray(matches)) {
|
|
150
|
+
files.push(...matches)
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
// Skip invalid patterns
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Remove duplicates and sort
|
|
158
|
+
return [...new Set(files)].sort()
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('Error finding files:', error.message)
|
|
161
|
+
return []
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Estimate context size (number of files)
|
|
167
|
+
*/
|
|
168
|
+
async estimateContextSize(taskAnalysis, projectPath) {
|
|
169
|
+
const files = await this.estimateFiles(taskAnalysis, projectPath)
|
|
170
|
+
return files.length
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = ContextEstimator
|
|
175
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modern Product Standards
|
|
3
|
+
* Defines what "Good" looks like for each domain.
|
|
4
|
+
* Used to inject high standards into agent prompts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const ProductStandards = {
|
|
8
|
+
// General standards applicable to all agents
|
|
9
|
+
general: [
|
|
10
|
+
'SHIP IT: Bias for action. Better to ship and iterate than perfect and delay.',
|
|
11
|
+
'USER CENTRIC: Always ask "How does this help the user?"',
|
|
12
|
+
'CLEAN CODE: Write code that is easy to read, test, and maintain.',
|
|
13
|
+
'NO BS: Avoid over-engineering. Simple is better than complex.'
|
|
14
|
+
],
|
|
15
|
+
|
|
16
|
+
// Domain-specific standards
|
|
17
|
+
domains: {
|
|
18
|
+
frontend: {
|
|
19
|
+
title: 'Modern Frontend Standards',
|
|
20
|
+
rules: [
|
|
21
|
+
'PERFORMANCE: Core Web Vitals matter. Optimize LCP, CLS, FID.',
|
|
22
|
+
'ACCESSIBILITY: Semantic HTML, ARIA labels, keyboard navigation (WCAG 2.1 AA).',
|
|
23
|
+
'RESPONSIVE: Mobile-first design. Works on all devices.',
|
|
24
|
+
'UX/UI: Smooth transitions, loading states, error boundaries. No dead clicks.',
|
|
25
|
+
'STATE: Local state for UI, Global state (Context/Zustand) for data. No prop drilling.'
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
backend: {
|
|
29
|
+
title: 'Robust Backend Standards',
|
|
30
|
+
rules: [
|
|
31
|
+
'SECURITY: Validate ALL inputs. Sanitize outputs. OWASP Top 10 awareness.',
|
|
32
|
+
'SCALABILITY: Stateless services. Caching strategies (Redis/CDN).',
|
|
33
|
+
'RELIABILITY: Graceful error handling. Structured logging. Health checks.',
|
|
34
|
+
'API DESIGN: RESTful or GraphQL best practices. Consistent response envelopes.',
|
|
35
|
+
'DB: Indexed queries. Migrations for schema changes. No N+1 queries.'
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
database: {
|
|
39
|
+
title: 'Data Integrity Standards',
|
|
40
|
+
rules: [
|
|
41
|
+
'INTEGRITY: Foreign keys, constraints, transactions.',
|
|
42
|
+
'PERFORMANCE: Index usage analysis. Query optimization.',
|
|
43
|
+
'BACKUPS: Point-in-time recovery awareness.',
|
|
44
|
+
'MIGRATIONS: Idempotent scripts. Zero-downtime changes.'
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
devops: {
|
|
48
|
+
title: 'Modern Ops Standards',
|
|
49
|
+
rules: [
|
|
50
|
+
'AUTOMATION: CI/CD for everything. No manual deployments.',
|
|
51
|
+
'IaC: Infrastructure as Code (Terraform/Pulumi).',
|
|
52
|
+
'OBSERVABILITY: Metrics, Logs, Traces (OpenTelemetry).',
|
|
53
|
+
'SECURITY: Least privilege access. Secrets management.'
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
qa: {
|
|
57
|
+
title: 'Quality Assurance Standards',
|
|
58
|
+
rules: [
|
|
59
|
+
'PYRAMID: Many unit tests, some integration, few E2E.',
|
|
60
|
+
'COVERAGE: Critical paths must be tested.',
|
|
61
|
+
'REALISM: Test with realistic data and scenarios.',
|
|
62
|
+
'SPEED: Fast feedback loops. Parallel execution.'
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
architecture: {
|
|
66
|
+
title: 'System Architecture Standards',
|
|
67
|
+
rules: [
|
|
68
|
+
'MODULARITY: High cohesion, low coupling.',
|
|
69
|
+
'EVOLVABILITY: Easy to change. Hard to break.',
|
|
70
|
+
'SIMPLICITY: Choose boring technology. Innovation tokens are limited.',
|
|
71
|
+
'DOCS: Architecture Decision Records (ADRs).'
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get standards for a specific domain
|
|
78
|
+
* @param {string} domain - The domain (frontend, backend, etc.)
|
|
79
|
+
* @returns {Object} The standards object
|
|
80
|
+
*/
|
|
81
|
+
getStandards(domain) {
|
|
82
|
+
const key = domain?.toLowerCase();
|
|
83
|
+
const specific = this.domains[key] || { title: 'General Standards', rules: [] };
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
title: specific.title,
|
|
87
|
+
rules: [...this.general, ...specific.rules]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
module.exports = ProductStandards;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCache - Intelligent Persistent Cache for Agents
|
|
3
|
+
*
|
|
4
|
+
* Cache with specific keys: {projectId}-{domain}-{techStack}
|
|
5
|
+
* Persists to disk for cross-session caching
|
|
6
|
+
* Invalidates only when stack changes
|
|
7
|
+
*
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs').promises
|
|
12
|
+
const path = require('path')
|
|
13
|
+
const os = require('os')
|
|
14
|
+
const crypto = require('crypto')
|
|
15
|
+
|
|
16
|
+
class SmartCache {
|
|
17
|
+
constructor(projectId = null) {
|
|
18
|
+
this.projectId = projectId
|
|
19
|
+
this.memoryCache = new Map()
|
|
20
|
+
this.cacheDir = path.join(os.homedir(), '.prjct-cli', 'cache')
|
|
21
|
+
this.cacheFile = projectId
|
|
22
|
+
? path.join(this.cacheDir, `agents-${projectId}.json`)
|
|
23
|
+
: path.join(this.cacheDir, 'agents-global.json')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize cache - load from disk
|
|
28
|
+
*/
|
|
29
|
+
async initialize() {
|
|
30
|
+
try {
|
|
31
|
+
await fs.mkdir(this.cacheDir, { recursive: true })
|
|
32
|
+
await this.loadFromDisk()
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Cache file doesn't exist yet - that's ok
|
|
35
|
+
this.memoryCache = new Map()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate cache key
|
|
41
|
+
* Format: {projectId}-{domain}-{techStackHash}
|
|
42
|
+
*/
|
|
43
|
+
generateKey(projectId, domain, techStack = {}) {
|
|
44
|
+
const techString = JSON.stringify(techStack)
|
|
45
|
+
const techHash = crypto.createHash('md5').update(techString).digest('hex').substring(0, 8)
|
|
46
|
+
return `${projectId || 'global'}-${domain}-${techHash}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get from cache
|
|
51
|
+
*/
|
|
52
|
+
async get(key) {
|
|
53
|
+
// Check memory first
|
|
54
|
+
if (this.memoryCache.has(key)) {
|
|
55
|
+
return this.memoryCache.get(key)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Load from disk if not in memory
|
|
59
|
+
await this.loadFromDisk()
|
|
60
|
+
return this.memoryCache.get(key) || null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Set in cache
|
|
65
|
+
*/
|
|
66
|
+
async set(key, value) {
|
|
67
|
+
// Set in memory
|
|
68
|
+
this.memoryCache.set(key, value)
|
|
69
|
+
|
|
70
|
+
// Persist to disk (async, don't wait)
|
|
71
|
+
this.persistToDisk().catch(err => {
|
|
72
|
+
console.error('Cache persist error:', err.message)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if key exists
|
|
78
|
+
*/
|
|
79
|
+
async has(key) {
|
|
80
|
+
if (this.memoryCache.has(key)) {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await this.loadFromDisk()
|
|
85
|
+
return this.memoryCache.has(key)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Clear cache
|
|
90
|
+
*/
|
|
91
|
+
async clear() {
|
|
92
|
+
this.memoryCache.clear()
|
|
93
|
+
try {
|
|
94
|
+
await fs.unlink(this.cacheFile)
|
|
95
|
+
} catch {
|
|
96
|
+
// File doesn't exist - that's ok
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Invalidate cache for a project (when stack changes)
|
|
102
|
+
*/
|
|
103
|
+
async invalidateProject(projectId) {
|
|
104
|
+
const keysToDelete = []
|
|
105
|
+
for (const key of this.memoryCache.keys()) {
|
|
106
|
+
if (key.startsWith(`${projectId}-`)) {
|
|
107
|
+
keysToDelete.push(key)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
keysToDelete.forEach(key => this.memoryCache.delete(key))
|
|
112
|
+
await this.persistToDisk()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Load cache from disk
|
|
117
|
+
*/
|
|
118
|
+
async loadFromDisk() {
|
|
119
|
+
try {
|
|
120
|
+
const content = await fs.readFile(this.cacheFile, 'utf-8')
|
|
121
|
+
const data = JSON.parse(content)
|
|
122
|
+
|
|
123
|
+
// Restore to memory cache
|
|
124
|
+
for (const [key, value] of Object.entries(data)) {
|
|
125
|
+
this.memoryCache.set(key, value)
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// File doesn't exist or invalid - start fresh
|
|
129
|
+
this.memoryCache = new Map()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Persist cache to disk
|
|
135
|
+
*/
|
|
136
|
+
async persistToDisk() {
|
|
137
|
+
try {
|
|
138
|
+
const data = Object.fromEntries(this.memoryCache)
|
|
139
|
+
await fs.writeFile(this.cacheFile, JSON.stringify(data, null, 2), 'utf-8')
|
|
140
|
+
} catch (error) {
|
|
141
|
+
// Fail silently - cache is best effort
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get cache statistics
|
|
147
|
+
*/
|
|
148
|
+
getStats() {
|
|
149
|
+
return {
|
|
150
|
+
size: this.memoryCache.size,
|
|
151
|
+
keys: Array.from(this.memoryCache.keys())
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = SmartCache
|
|
157
|
+
|