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
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
-
import templateLoader from '../../agentic/template-loader.js'
|
|
3
|
-
|
|
4
|
-
describe('Template Loader', () => {
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
templateLoader.clearCache()
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
describe('load()', () => {
|
|
10
|
-
it('should load a template successfully', async () => {
|
|
11
|
-
const template = await templateLoader.load('now')
|
|
12
|
-
|
|
13
|
-
expect(template).toBeDefined()
|
|
14
|
-
expect(template).toHaveProperty('content')
|
|
15
|
-
expect(template).toHaveProperty('frontmatter')
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('should load multiple templates', async () => {
|
|
19
|
-
const now = await templateLoader.load('now')
|
|
20
|
-
const done = await templateLoader.load('done')
|
|
21
|
-
const next = await templateLoader.load('next')
|
|
22
|
-
|
|
23
|
-
expect(now).toBeDefined()
|
|
24
|
-
expect(done).toBeDefined()
|
|
25
|
-
expect(next).toBeDefined()
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('should throw error for non-existent template', async () => {
|
|
29
|
-
await expect(templateLoader.load('nonexistent')).rejects.toThrow('Template not found: nonexistent.md')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('should cache templates', async () => {
|
|
33
|
-
const first = await templateLoader.load('now')
|
|
34
|
-
const second = await templateLoader.load('now')
|
|
35
|
-
|
|
36
|
-
// Should return the same cached object
|
|
37
|
-
expect(first).toBe(second)
|
|
38
|
-
})
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
describe('parseFrontmatter()', () => {
|
|
42
|
-
it('should parse frontmatter correctly', () => {
|
|
43
|
-
const content = `---
|
|
44
|
-
allowed-tools: [Read, Write, Bash]
|
|
45
|
-
category: core
|
|
46
|
-
---
|
|
47
|
-
# Template Content
|
|
48
|
-
|
|
49
|
-
This is the main content.`
|
|
50
|
-
|
|
51
|
-
const parsed = templateLoader.parseFrontmatter(content)
|
|
52
|
-
|
|
53
|
-
expect(parsed.frontmatter).toHaveProperty('allowed-tools')
|
|
54
|
-
expect(parsed.frontmatter['allowed-tools']).toEqual(['Read', 'Write', 'Bash'])
|
|
55
|
-
expect(parsed.frontmatter.category).toBe('core')
|
|
56
|
-
expect(parsed.content).toContain('# Template Content')
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('should handle templates without frontmatter', () => {
|
|
60
|
-
const content = `# Simple Template
|
|
61
|
-
|
|
62
|
-
Just content, no frontmatter.`
|
|
63
|
-
|
|
64
|
-
const parsed = templateLoader.parseFrontmatter(content)
|
|
65
|
-
|
|
66
|
-
expect(parsed.frontmatter).toEqual({})
|
|
67
|
-
expect(parsed.content).toContain('# Simple Template')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('should parse string values', () => {
|
|
71
|
-
const content = `---
|
|
72
|
-
title: Test Command
|
|
73
|
-
description: A test command
|
|
74
|
-
---
|
|
75
|
-
Content`
|
|
76
|
-
|
|
77
|
-
const parsed = templateLoader.parseFrontmatter(content)
|
|
78
|
-
|
|
79
|
-
expect(parsed.frontmatter.title).toBe('Test Command')
|
|
80
|
-
expect(parsed.frontmatter.description).toBe('A test command')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('should handle quoted strings', () => {
|
|
84
|
-
const content = `---
|
|
85
|
-
title: "Quoted Title"
|
|
86
|
-
description: 'Single Quoted'
|
|
87
|
-
---
|
|
88
|
-
Content`
|
|
89
|
-
|
|
90
|
-
const parsed = templateLoader.parseFrontmatter(content)
|
|
91
|
-
|
|
92
|
-
expect(parsed.frontmatter.title).toBe('Quoted Title')
|
|
93
|
-
expect(parsed.frontmatter.description).toBe('Single Quoted')
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('should parse array values', () => {
|
|
97
|
-
const content = `---
|
|
98
|
-
tools: [Read, Write, Exec]
|
|
99
|
-
tags: [core, important]
|
|
100
|
-
---
|
|
101
|
-
Content`
|
|
102
|
-
|
|
103
|
-
const parsed = templateLoader.parseFrontmatter(content)
|
|
104
|
-
|
|
105
|
-
expect(Array.isArray(parsed.frontmatter.tools)).toBe(true)
|
|
106
|
-
expect(parsed.frontmatter.tools).toEqual(['Read', 'Write', 'Exec'])
|
|
107
|
-
expect(parsed.frontmatter.tags).toEqual(['core', 'important'])
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
describe('getAllowedTools()', () => {
|
|
112
|
-
it('should return allowed tools from template', async () => {
|
|
113
|
-
const tools = await templateLoader.getAllowedTools('now')
|
|
114
|
-
|
|
115
|
-
expect(Array.isArray(tools)).toBe(true)
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('should return empty array if no allowed-tools defined', async () => {
|
|
119
|
-
// Create a mock template without allowed-tools
|
|
120
|
-
const content = `# Simple Template
|
|
121
|
-
No tools defined`
|
|
122
|
-
|
|
123
|
-
const parsed = templateLoader.parseFrontmatter(content)
|
|
124
|
-
expect(parsed.frontmatter['allowed-tools'] || []).toEqual([])
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
describe('clearCache()', () => {
|
|
129
|
-
it('should clear the cache', async () => {
|
|
130
|
-
// Load and cache
|
|
131
|
-
await templateLoader.load('now')
|
|
132
|
-
|
|
133
|
-
// Clear cache
|
|
134
|
-
templateLoader.clearCache()
|
|
135
|
-
|
|
136
|
-
// Load again - should read from file, not cache
|
|
137
|
-
const template = await templateLoader.load('now')
|
|
138
|
-
expect(template).toBeDefined()
|
|
139
|
-
})
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
describe('Real Templates', () => {
|
|
143
|
-
it('should load "now" template', async () => {
|
|
144
|
-
const template = await templateLoader.load('now')
|
|
145
|
-
|
|
146
|
-
expect(template.content).toBeTruthy()
|
|
147
|
-
expect(template.content.length).toBeGreaterThan(0)
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('should load "done" template', async () => {
|
|
151
|
-
const template = await templateLoader.load('done')
|
|
152
|
-
|
|
153
|
-
expect(template.content).toBeTruthy()
|
|
154
|
-
expect(template.content.length).toBeGreaterThan(0)
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
it('should load "ship" template', async () => {
|
|
158
|
-
const template = await templateLoader.load('ship')
|
|
159
|
-
|
|
160
|
-
expect(template.content).toBeTruthy()
|
|
161
|
-
expect(template.content.length).toBeGreaterThan(0)
|
|
162
|
-
})
|
|
163
|
-
})
|
|
164
|
-
})
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import toolRegistry from '../../agentic/tool-registry.js'
|
|
3
|
-
import fs from 'fs/promises'
|
|
4
|
-
import path from 'path'
|
|
5
|
-
import os from 'os'
|
|
6
|
-
|
|
7
|
-
describe('Tool Registry', () => {
|
|
8
|
-
const testDir = path.join(os.tmpdir(), 'prjct-test-' + Date.now())
|
|
9
|
-
const testFile = path.join(testDir, 'test.txt')
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
await fs.mkdir(testDir, { recursive: true })
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
afterEach(async () => {
|
|
16
|
-
try {
|
|
17
|
-
await fs.rm(testDir, { recursive: true, force: true })
|
|
18
|
-
} catch (error) {
|
|
19
|
-
// Ignore cleanup errors
|
|
20
|
-
}
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
describe('get()', () => {
|
|
24
|
-
it('should get Read tool', () => {
|
|
25
|
-
const tool = toolRegistry.get('Read')
|
|
26
|
-
|
|
27
|
-
expect(tool).toBeDefined()
|
|
28
|
-
expect(typeof tool).toBe('function')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('should get Write tool', () => {
|
|
32
|
-
const tool = toolRegistry.get('Write')
|
|
33
|
-
|
|
34
|
-
expect(tool).toBeDefined()
|
|
35
|
-
expect(typeof tool).toBe('function')
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('should get Bash tool', () => {
|
|
39
|
-
const tool = toolRegistry.get('Bash')
|
|
40
|
-
|
|
41
|
-
expect(tool).toBeDefined()
|
|
42
|
-
expect(typeof tool).toBe('function')
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('should get Exec tool (alias for Bash)', () => {
|
|
46
|
-
const tool = toolRegistry.get('Exec')
|
|
47
|
-
|
|
48
|
-
expect(tool).toBeDefined()
|
|
49
|
-
expect(typeof tool).toBe('function')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('should throw error for unknown tool', () => {
|
|
53
|
-
expect(() => toolRegistry.get('UnknownTool')).toThrow('Unknown tool: UnknownTool')
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
describe('isAllowed()', () => {
|
|
58
|
-
it('should return true for allowed tool', () => {
|
|
59
|
-
const allowed = toolRegistry.isAllowed('Read', ['Read', 'Write', 'Bash'])
|
|
60
|
-
|
|
61
|
-
expect(allowed).toBe(true)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('should return false for disallowed tool', () => {
|
|
65
|
-
const allowed = toolRegistry.isAllowed('Exec', ['Read', 'Write'])
|
|
66
|
-
|
|
67
|
-
expect(allowed).toBe(false)
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('should handle empty allowed list', () => {
|
|
71
|
-
const allowed = toolRegistry.isAllowed('Read', [])
|
|
72
|
-
|
|
73
|
-
expect(allowed).toBe(false)
|
|
74
|
-
})
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
describe('read()', () => {
|
|
78
|
-
it('should read file content', async () => {
|
|
79
|
-
const content = 'Test content for reading'
|
|
80
|
-
await fs.writeFile(testFile, content)
|
|
81
|
-
|
|
82
|
-
const result = await toolRegistry.read(testFile)
|
|
83
|
-
|
|
84
|
-
expect(result).toBe(content)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('should return null for non-existent file', async () => {
|
|
88
|
-
const result = await toolRegistry.read('/nonexistent/file.txt')
|
|
89
|
-
|
|
90
|
-
expect(result).toBeNull()
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('should read UTF-8 encoded content', async () => {
|
|
94
|
-
const content = 'UTF-8: ñ é ü 中文 日本語'
|
|
95
|
-
await fs.writeFile(testFile, content)
|
|
96
|
-
|
|
97
|
-
const result = await toolRegistry.read(testFile)
|
|
98
|
-
|
|
99
|
-
expect(result).toBe(content)
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
describe('write()', () => {
|
|
104
|
-
it('should write file content', async () => {
|
|
105
|
-
const content = 'Test content for writing'
|
|
106
|
-
|
|
107
|
-
await toolRegistry.write(testFile, content)
|
|
108
|
-
|
|
109
|
-
const result = await fs.readFile(testFile, 'utf-8')
|
|
110
|
-
expect(result).toBe(content)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('should create directory if not exists', async () => {
|
|
114
|
-
const nestedFile = path.join(testDir, 'nested', 'dir', 'file.txt')
|
|
115
|
-
const content = 'Nested content'
|
|
116
|
-
|
|
117
|
-
await toolRegistry.write(nestedFile, content)
|
|
118
|
-
|
|
119
|
-
const result = await fs.readFile(nestedFile, 'utf-8')
|
|
120
|
-
expect(result).toBe(content)
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('should overwrite existing file', async () => {
|
|
124
|
-
await toolRegistry.write(testFile, 'First content')
|
|
125
|
-
await toolRegistry.write(testFile, 'Second content')
|
|
126
|
-
|
|
127
|
-
const result = await fs.readFile(testFile, 'utf-8')
|
|
128
|
-
expect(result).toBe('Second content')
|
|
129
|
-
})
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
describe('bash()', () => {
|
|
133
|
-
it('should execute simple command', async () => {
|
|
134
|
-
const result = await toolRegistry.bash('echo "hello"')
|
|
135
|
-
|
|
136
|
-
expect(result.stdout).toContain('hello')
|
|
137
|
-
expect(result.stderr).toBe('')
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('should return stdout and stderr', async () => {
|
|
141
|
-
const result = await toolRegistry.bash('echo "output"')
|
|
142
|
-
|
|
143
|
-
expect(result).toHaveProperty('stdout')
|
|
144
|
-
expect(result).toHaveProperty('stderr')
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('should handle command errors', async () => {
|
|
148
|
-
const result = await toolRegistry.bash('invalid-command-xyz-123')
|
|
149
|
-
|
|
150
|
-
expect(result.error).toBe(true)
|
|
151
|
-
expect(result.stderr).toBeTruthy()
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it('should execute pwd command', async () => {
|
|
155
|
-
const result = await toolRegistry.bash('pwd')
|
|
156
|
-
|
|
157
|
-
expect(result.stdout).toBeTruthy()
|
|
158
|
-
expect(result.stderr).toBe('')
|
|
159
|
-
})
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
describe('exists()', () => {
|
|
163
|
-
it('should return true for existing file', async () => {
|
|
164
|
-
await fs.writeFile(testFile, 'content')
|
|
165
|
-
|
|
166
|
-
const exists = await toolRegistry.exists(testFile)
|
|
167
|
-
|
|
168
|
-
expect(exists).toBe(true)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('should return false for non-existent file', async () => {
|
|
172
|
-
const exists = await toolRegistry.exists('/nonexistent/file.txt')
|
|
173
|
-
|
|
174
|
-
expect(exists).toBe(false)
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it('should work with directories', async () => {
|
|
178
|
-
const exists = await toolRegistry.exists(testDir)
|
|
179
|
-
|
|
180
|
-
expect(exists).toBe(true)
|
|
181
|
-
})
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
describe('list()', () => {
|
|
185
|
-
it('should list directory contents', async () => {
|
|
186
|
-
await fs.writeFile(path.join(testDir, 'file1.txt'), 'content1')
|
|
187
|
-
await fs.writeFile(path.join(testDir, 'file2.txt'), 'content2')
|
|
188
|
-
|
|
189
|
-
const files = await toolRegistry.list(testDir)
|
|
190
|
-
|
|
191
|
-
expect(Array.isArray(files)).toBe(true)
|
|
192
|
-
expect(files.length).toBeGreaterThanOrEqual(2)
|
|
193
|
-
expect(files).toContain('file1.txt')
|
|
194
|
-
expect(files).toContain('file2.txt')
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('should return empty array for non-existent directory', async () => {
|
|
198
|
-
const files = await toolRegistry.list('/nonexistent/directory')
|
|
199
|
-
|
|
200
|
-
expect(files).toEqual([])
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
it('should list empty directory', async () => {
|
|
204
|
-
const emptyDir = path.join(testDir, 'empty')
|
|
205
|
-
await fs.mkdir(emptyDir)
|
|
206
|
-
|
|
207
|
-
const files = await toolRegistry.list(emptyDir)
|
|
208
|
-
|
|
209
|
-
expect(Array.isArray(files)).toBe(true)
|
|
210
|
-
expect(files.length).toBe(0)
|
|
211
|
-
})
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
describe('Integration', () => {
|
|
215
|
-
it('should write and then read file', async () => {
|
|
216
|
-
const content = 'Integration test content'
|
|
217
|
-
|
|
218
|
-
await toolRegistry.write(testFile, content)
|
|
219
|
-
const result = await toolRegistry.read(testFile)
|
|
220
|
-
|
|
221
|
-
expect(result).toBe(content)
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('should check existence, write, and list', async () => {
|
|
225
|
-
const newFile = path.join(testDir, 'integration.txt')
|
|
226
|
-
|
|
227
|
-
// Check doesn't exist
|
|
228
|
-
const existsBefore = await toolRegistry.exists(newFile)
|
|
229
|
-
expect(existsBefore).toBe(false)
|
|
230
|
-
|
|
231
|
-
// Write
|
|
232
|
-
await toolRegistry.write(newFile, 'content')
|
|
233
|
-
|
|
234
|
-
// Check exists
|
|
235
|
-
const existsAfter = await toolRegistry.exists(newFile)
|
|
236
|
-
expect(existsAfter).toBe(true)
|
|
237
|
-
|
|
238
|
-
// List directory
|
|
239
|
-
const files = await toolRegistry.list(testDir)
|
|
240
|
-
expect(files).toContain('integration.txt')
|
|
241
|
-
})
|
|
242
|
-
})
|
|
243
|
-
})
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import AgentGenerator from '../../domain/agent-generator.js'
|
|
3
|
-
import fs from 'fs/promises'
|
|
4
|
-
import os from 'os'
|
|
5
|
-
import path from 'path'
|
|
6
|
-
|
|
7
|
-
describe('Agent Generator', () => {
|
|
8
|
-
const testProjectId = 'test-agent-gen-' + Date.now()
|
|
9
|
-
let generator
|
|
10
|
-
let agentsDir
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
generator = new AgentGenerator(testProjectId)
|
|
14
|
-
agentsDir = path.join(os.homedir(), '.prjct-cli', 'projects', testProjectId, 'agents')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
afterEach(async () => {
|
|
18
|
-
// Cleanup test files
|
|
19
|
-
try {
|
|
20
|
-
await fs.rm(agentsDir, { recursive: true, force: true })
|
|
21
|
-
} catch (error) {
|
|
22
|
-
// Ignore cleanup errors
|
|
23
|
-
}
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
describe('Constructor', () => {
|
|
27
|
-
it('should create generator with project ID', () => {
|
|
28
|
-
expect(generator.projectId).toBe(testProjectId)
|
|
29
|
-
expect(generator.outputDir).toContain(testProjectId)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('should use fallback directory without project ID', () => {
|
|
33
|
-
const fallbackGenerator = new AgentGenerator()
|
|
34
|
-
expect(fallbackGenerator.outputDir).toContain('.prjct-cli/agents')
|
|
35
|
-
expect(fallbackGenerator.outputDir).not.toContain('projects')
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('should construct correct output path', () => {
|
|
39
|
-
expect(generator.outputDir).toBe(agentsDir)
|
|
40
|
-
})
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
describe('generateDynamicAgent()', () => {
|
|
44
|
-
it('should generate agent file', async () => {
|
|
45
|
-
await generator.generateDynamicAgent('test-agent', {
|
|
46
|
-
role: 'Test Agent Role',
|
|
47
|
-
expertise: 'Test Technologies',
|
|
48
|
-
responsibilities: 'Test Responsibilities',
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
const agentFile = path.join(agentsDir, 'test-agent.md')
|
|
52
|
-
const exists = await fs
|
|
53
|
-
.access(agentFile)
|
|
54
|
-
.then(() => true)
|
|
55
|
-
.catch(() => false)
|
|
56
|
-
|
|
57
|
-
expect(exists).toBe(true)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('should create agent with correct content', async () => {
|
|
61
|
-
await generator.generateDynamicAgent('backend-agent', {
|
|
62
|
-
role: 'Backend Developer',
|
|
63
|
-
expertise: 'Node.js, Express, PostgreSQL',
|
|
64
|
-
responsibilities: 'API development and database management',
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
const content = await fs.readFile(path.join(agentsDir, 'backend-agent.md'), 'utf-8')
|
|
68
|
-
|
|
69
|
-
expect(content).toContain('# Backend Developer')
|
|
70
|
-
expect(content).toContain('## Role')
|
|
71
|
-
expect(content).toContain('Backend Developer')
|
|
72
|
-
expect(content).toContain('## Expertise')
|
|
73
|
-
expect(content).toContain('Node.js, Express, PostgreSQL')
|
|
74
|
-
expect(content).toContain('## Responsibilities')
|
|
75
|
-
expect(content).toContain('API development and database management')
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('should include project context in agent file', async () => {
|
|
79
|
-
await generator.generateDynamicAgent('context-agent', {
|
|
80
|
-
role: 'Agent with Context',
|
|
81
|
-
expertise: 'Testing',
|
|
82
|
-
responsibilities: 'Test things',
|
|
83
|
-
projectContext: {
|
|
84
|
-
framework: 'React',
|
|
85
|
-
version: '18.0',
|
|
86
|
-
},
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
const content = await fs.readFile(path.join(agentsDir, 'context-agent.md'), 'utf-8')
|
|
90
|
-
|
|
91
|
-
expect(content).toContain('## Project Context')
|
|
92
|
-
expect(content).toContain('framework')
|
|
93
|
-
expect(content).toContain('React')
|
|
94
|
-
expect(content).toContain('version')
|
|
95
|
-
expect(content).toContain('18.0')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('should handle missing optional fields', async () => {
|
|
99
|
-
await generator.generateDynamicAgent('minimal-agent', {
|
|
100
|
-
role: 'Minimal Role',
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
const content = await fs.readFile(path.join(agentsDir, 'minimal-agent.md'), 'utf-8')
|
|
104
|
-
|
|
105
|
-
expect(content).toContain('# Minimal Role')
|
|
106
|
-
expect(content).toContain('Technologies used in this project')
|
|
107
|
-
expect(content).toContain('Handle specific aspects of development')
|
|
108
|
-
expect(content).toContain('No additional context')
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('should create output directory if not exists', async () => {
|
|
112
|
-
const newProjectId = 'new-project-' + Date.now()
|
|
113
|
-
const newGenerator = new AgentGenerator(newProjectId)
|
|
114
|
-
const newAgentsDir = path.join(os.homedir(), '.prjct-cli', 'projects', newProjectId, 'agents')
|
|
115
|
-
|
|
116
|
-
await newGenerator.generateDynamicAgent('auto-create', {
|
|
117
|
-
role: 'Auto Created Agent',
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
const exists = await fs
|
|
121
|
-
.access(newAgentsDir)
|
|
122
|
-
.then(() => true)
|
|
123
|
-
.catch(() => false)
|
|
124
|
-
|
|
125
|
-
expect(exists).toBe(true)
|
|
126
|
-
|
|
127
|
-
// Cleanup
|
|
128
|
-
await fs.rm(path.join(os.homedir(), '.prjct-cli', 'projects', newProjectId), {
|
|
129
|
-
recursive: true,
|
|
130
|
-
force: true,
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it('should create multiple agents', async () => {
|
|
135
|
-
await generator.generateDynamicAgent('agent-1', { role: 'Agent One' })
|
|
136
|
-
await generator.generateDynamicAgent('agent-2', { role: 'Agent Two' })
|
|
137
|
-
await generator.generateDynamicAgent('agent-3', { role: 'Agent Three' })
|
|
138
|
-
|
|
139
|
-
const agents = await generator.listAgents()
|
|
140
|
-
|
|
141
|
-
expect(agents).toHaveLength(3)
|
|
142
|
-
expect(agents).toContain('agent-1')
|
|
143
|
-
expect(agents).toContain('agent-2')
|
|
144
|
-
expect(agents).toContain('agent-3')
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('should use agent name as fallback for role', async () => {
|
|
148
|
-
await generator.generateDynamicAgent('fallback-agent', {})
|
|
149
|
-
|
|
150
|
-
const content = await fs.readFile(path.join(agentsDir, 'fallback-agent.md'), 'utf-8')
|
|
151
|
-
|
|
152
|
-
expect(content).toContain('# fallback-agent')
|
|
153
|
-
})
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
describe('cleanupObsoleteAgents()', () => {
|
|
157
|
-
beforeEach(async () => {
|
|
158
|
-
// Create some test agents
|
|
159
|
-
await generator.generateDynamicAgent('agent-1', { role: 'Agent 1' })
|
|
160
|
-
await generator.generateDynamicAgent('agent-2', { role: 'Agent 2' })
|
|
161
|
-
await generator.generateDynamicAgent('agent-3', { role: 'Agent 3' })
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('should remove obsolete agents', async () => {
|
|
165
|
-
const removed = await generator.cleanupObsoleteAgents(['agent-1', 'agent-2'])
|
|
166
|
-
|
|
167
|
-
expect(removed).toContain('agent-3')
|
|
168
|
-
expect(removed).toHaveLength(1)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('should keep required agents', async () => {
|
|
172
|
-
await generator.cleanupObsoleteAgents(['agent-1', 'agent-2'])
|
|
173
|
-
|
|
174
|
-
const agents = await generator.listAgents()
|
|
175
|
-
|
|
176
|
-
expect(agents).toContain('agent-1')
|
|
177
|
-
expect(agents).toContain('agent-2')
|
|
178
|
-
expect(agents).not.toContain('agent-3')
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
it('should remove multiple obsolete agents', async () => {
|
|
182
|
-
await generator.generateDynamicAgent('agent-4', { role: 'Agent 4' })
|
|
183
|
-
|
|
184
|
-
const removed = await generator.cleanupObsoleteAgents(['agent-1'])
|
|
185
|
-
|
|
186
|
-
expect(removed).toHaveLength(3)
|
|
187
|
-
expect(removed).toContain('agent-2')
|
|
188
|
-
expect(removed).toContain('agent-3')
|
|
189
|
-
expect(removed).toContain('agent-4')
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
it('should return empty array if all agents are required', async () => {
|
|
193
|
-
const removed = await generator.cleanupObsoleteAgents(['agent-1', 'agent-2', 'agent-3'])
|
|
194
|
-
|
|
195
|
-
expect(removed).toEqual([])
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('should handle non-existent directory gracefully', async () => {
|
|
199
|
-
const emptyGenerator = new AgentGenerator('empty-' + Date.now())
|
|
200
|
-
|
|
201
|
-
const removed = await emptyGenerator.cleanupObsoleteAgents(['agent-1'])
|
|
202
|
-
|
|
203
|
-
expect(Array.isArray(removed)).toBe(true)
|
|
204
|
-
})
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
describe('listAgents()', () => {
|
|
208
|
-
it('should list all agents', async () => {
|
|
209
|
-
await generator.generateDynamicAgent('frontend', { role: 'Frontend' })
|
|
210
|
-
await generator.generateDynamicAgent('backend', { role: 'Backend' })
|
|
211
|
-
|
|
212
|
-
const agents = await generator.listAgents()
|
|
213
|
-
|
|
214
|
-
expect(agents).toHaveLength(2)
|
|
215
|
-
expect(agents).toContain('frontend')
|
|
216
|
-
expect(agents).toContain('backend')
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('should return empty array for no agents', async () => {
|
|
220
|
-
const agents = await generator.listAgents()
|
|
221
|
-
|
|
222
|
-
expect(agents).toEqual([])
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it('should ignore non-.md files', async () => {
|
|
226
|
-
await generator.generateDynamicAgent('valid-agent', { role: 'Valid' })
|
|
227
|
-
await fs.writeFile(path.join(agentsDir, 'not-agent.txt'), 'text file')
|
|
228
|
-
await fs.writeFile(path.join(agentsDir, 'config.json'), '{}')
|
|
229
|
-
|
|
230
|
-
const agents = await generator.listAgents()
|
|
231
|
-
|
|
232
|
-
expect(agents).toHaveLength(1)
|
|
233
|
-
expect(agents).toContain('valid-agent')
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
it('should ignore hidden files', async () => {
|
|
237
|
-
await generator.generateDynamicAgent('visible', { role: 'Visible' })
|
|
238
|
-
await fs.writeFile(path.join(agentsDir, '.hidden.md'), 'hidden')
|
|
239
|
-
|
|
240
|
-
const agents = await generator.listAgents()
|
|
241
|
-
|
|
242
|
-
expect(agents).toHaveLength(1)
|
|
243
|
-
expect(agents).toContain('visible')
|
|
244
|
-
})
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
describe('Integration', () => {
|
|
248
|
-
it('should create, list, and cleanup agents', async () => {
|
|
249
|
-
// Create agents
|
|
250
|
-
await generator.generateDynamicAgent('keep-me', { role: 'Keep' })
|
|
251
|
-
await generator.generateDynamicAgent('remove-me', { role: 'Remove' })
|
|
252
|
-
|
|
253
|
-
// Verify they exist
|
|
254
|
-
let agents = await generator.listAgents()
|
|
255
|
-
expect(agents).toHaveLength(2)
|
|
256
|
-
|
|
257
|
-
// Cleanup obsolete
|
|
258
|
-
const removed = await generator.cleanupObsoleteAgents(['keep-me'])
|
|
259
|
-
expect(removed).toContain('remove-me')
|
|
260
|
-
|
|
261
|
-
// Verify cleanup
|
|
262
|
-
agents = await generator.listAgents()
|
|
263
|
-
expect(agents).toHaveLength(1)
|
|
264
|
-
expect(agents).toContain('keep-me')
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
it('should handle agent file content correctly', async () => {
|
|
268
|
-
await generator.generateDynamicAgent('full-agent', {
|
|
269
|
-
role: 'Full Stack Developer',
|
|
270
|
-
expertise: 'React, Node.js, PostgreSQL, Docker',
|
|
271
|
-
responsibilities: 'Build and deploy full stack applications',
|
|
272
|
-
projectContext: {
|
|
273
|
-
stack: 'MERN',
|
|
274
|
-
deployment: 'AWS',
|
|
275
|
-
},
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
const content = await fs.readFile(path.join(agentsDir, 'full-agent.md'), 'utf-8')
|
|
279
|
-
|
|
280
|
-
// Should have all sections
|
|
281
|
-
expect(content).toContain('# Full Stack Developer')
|
|
282
|
-
expect(content).toContain('## Role')
|
|
283
|
-
expect(content).toContain('## Expertise')
|
|
284
|
-
expect(content).toContain('## Responsibilities')
|
|
285
|
-
expect(content).toContain('## Project Context')
|
|
286
|
-
expect(content).toContain('## Guidelines')
|
|
287
|
-
|
|
288
|
-
// Should have all content
|
|
289
|
-
expect(content).toContain('Full Stack Developer')
|
|
290
|
-
expect(content).toContain('React, Node.js, PostgreSQL, Docker')
|
|
291
|
-
expect(content).toContain('Build and deploy full stack applications')
|
|
292
|
-
expect(content).toContain('MERN')
|
|
293
|
-
expect(content).toContain('AWS')
|
|
294
|
-
})
|
|
295
|
-
})
|
|
296
|
-
})
|