prjct-cli 0.28.0 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -12
- package/CLAUDE.md +17 -25
- package/core/agentic/agent-router.ts +15 -0
- package/core/agentic/command-executor.ts +53 -5
- package/core/agentic/prompt-builder.ts +81 -0
- package/core/agentic/template-loader.ts +107 -32
- package/core/domain/agent-loader.ts +7 -9
- package/core/domain/context-estimator.ts +15 -15
- package/core/infrastructure/config-manager.ts +25 -4
- package/core/infrastructure/setup.ts +0 -99
- package/core/session/session-log-manager.ts +17 -0
- package/dist/bin/prjct.mjs +339 -235
- package/package.json +1 -1
- package/templates/commands/cleanup.md +15 -74
- package/templates/commands/ship.md +5 -0
- package/templates/commands/task.md +41 -0
- package/templates/global/CLAUDE.md +196 -25
- package/templates/hooks/prjct-session-start.sh +0 -50
- package/templates/skills/prjct-done/SKILL.md +0 -97
- package/templates/skills/prjct-ship/SKILL.md +0 -150
- package/templates/skills/prjct-sync/SKILL.md +0 -108
- package/templates/skills/prjct-task/SKILL.md +0 -101
package/CHANGELOG.md
CHANGED
|
@@ -1,21 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [0.28.
|
|
3
|
+
## [0.28.1] - 2026-01-10
|
|
4
4
|
|
|
5
|
-
### Feature:
|
|
5
|
+
### Feature: @ Agent Mentions
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Direct agent invocation via @ notation in tasks.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
**New Capabilities:**
|
|
10
|
+
|
|
11
|
+
1. **@ Agent Mentions**
|
|
12
|
+
- `p. task @frontend add button` - Loads frontend specialist directly
|
|
13
|
+
- `p. task @frontend @uxui dark mode` - Loads multiple agents
|
|
14
|
+
- Supports all prjct agents: `@frontend`, `@backend`, `@database`, `@uxui`, `@testing`, `@devops`
|
|
15
|
+
|
|
16
|
+
2. **@ Claude Subagents**
|
|
17
|
+
- `@explore` - Fast codebase search (Task tool with subagent_type=Explore)
|
|
18
|
+
- `@general` - Complex multi-step research
|
|
19
|
+
- `@plan` - Architecture design and planning
|
|
20
|
+
- Example: `p. task @frontend @explore add button like existing ones`
|
|
17
21
|
|
|
18
|
-
**
|
|
22
|
+
3. **Performance Improvements**
|
|
23
|
+
- Template cache: O(1) LRU using ES6 Map insertion order
|
|
24
|
+
- Agent loading: Parallel Promise.all() for faster startup
|
|
25
|
+
- Glob searches: Parallel execution in context estimation
|
|
26
|
+
|
|
27
|
+
4. **Tool Permissions**
|
|
28
|
+
- Templates can specify `tool-permissions` in frontmatter
|
|
29
|
+
- Supports `allow`, `ask`, and `deny` rules per tool
|
|
30
|
+
- Example: `ship.md` now declares safe vs dangerous git commands
|
|
31
|
+
|
|
32
|
+
**Files Changed:**
|
|
33
|
+
- `core/agentic/command-executor.ts` - Parse @ mentions, load mentioned agents
|
|
34
|
+
- `core/agentic/prompt-builder.ts` - Build subagent instructions, tool permissions
|
|
35
|
+
- `core/agentic/template-loader.ts` - LRU cache optimization, tool-permissions parsing
|
|
36
|
+
- `core/agentic/agent-router.ts` - `loadAgentsByMention()` method
|
|
37
|
+
- `core/domain/agent-loader.ts` - Parallel agent loading
|
|
38
|
+
- `core/domain/context-estimator.ts` - Parallel glob execution
|
|
39
|
+
- `templates/commands/task.md` - @ mention documentation
|
|
19
40
|
|
|
20
41
|
---
|
|
21
42
|
|
package/CLAUDE.md
CHANGED
|
@@ -145,34 +145,26 @@ Next: [suggested action]
|
|
|
145
145
|
|
|
146
146
|
---
|
|
147
147
|
|
|
148
|
-
##
|
|
148
|
+
## SKILL INTEGRATION (v0.27)
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
Agents are linked to Claude Code skills from claude-plugins.dev.
|
|
151
151
|
|
|
152
|
-
###
|
|
152
|
+
### Agent → Skill Mapping
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
| Agent | Skill |
|
|
155
|
+
|-------|-------|
|
|
156
|
+
| `frontend.md` | `frontend-design` |
|
|
157
|
+
| `uxui.md` | `frontend-design` |
|
|
158
|
+
| `backend.md` | `javascript-typescript` |
|
|
159
|
+
| `testing.md` | `developer-kit` |
|
|
160
|
+
| `devops.md` | `developer-kit` |
|
|
161
|
+
| `prjct-planner.md` | `feature-dev` |
|
|
162
|
+
| `prjct-shipper.md` | `code-review` |
|
|
158
163
|
|
|
159
|
-
###
|
|
164
|
+
### Usage
|
|
160
165
|
|
|
161
|
-
|
|
166
|
+
- `p. sync` installs required skills automatically
|
|
167
|
+
- `p. task` invokes skills based on task type
|
|
168
|
+
- Skills config: `{globalPath}/config/skills.json`
|
|
162
169
|
|
|
163
|
-
|
|
164
|
-
|-------|---------|
|
|
165
|
-
| `prjct-task` | "p. task", starting work, features/bugs |
|
|
166
|
-
| `prjct-sync` | "p. sync", analyze codebase |
|
|
167
|
-
| `prjct-done` | "p. done", completing work |
|
|
168
|
-
| `prjct-ship` | "p. ship", releasing features |
|
|
169
|
-
|
|
170
|
-
Skills location: `~/.claude/skills/prjct-*/SKILL.md`
|
|
171
|
-
|
|
172
|
-
### Setup
|
|
173
|
-
|
|
174
|
-
All integration is installed automatically via `npm install -g prjct-cli`:
|
|
175
|
-
1. SessionStart hook → `~/.claude/hooks/`
|
|
176
|
-
2. Skills → `~/.claude/skills/`
|
|
177
|
-
3. Commands → `~/.claude/commands/p/`
|
|
178
|
-
4. Settings → `~/.claude/settings.json`
|
|
170
|
+
See `templates/agentic/skill-integration.md` for details.
|
|
@@ -104,6 +104,21 @@ class AgentRouter {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Load multiple agents by @ mention names
|
|
109
|
+
* @example loadAgentsByMention(["frontend", "uxui"]) -> [Agent, Agent]
|
|
110
|
+
*/
|
|
111
|
+
async loadAgentsByMention(mentions: string[]): Promise<Agent[]> {
|
|
112
|
+
const agents: Agent[] = []
|
|
113
|
+
for (const name of mentions) {
|
|
114
|
+
const agent = await this.loadAgent(name)
|
|
115
|
+
if (agent) {
|
|
116
|
+
agents.push(agent)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return agents
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
/**
|
|
108
123
|
* Log agent usage to JSONL file
|
|
109
124
|
*/
|
|
@@ -18,6 +18,7 @@ import chainOfThought from './chain-of-thought'
|
|
|
18
18
|
import memorySystem from './memory-system'
|
|
19
19
|
import groundTruth from './ground-truth'
|
|
20
20
|
import planMode from './plan-mode'
|
|
21
|
+
import AgentRouter from './agent-router'
|
|
21
22
|
|
|
22
23
|
import type {
|
|
23
24
|
ExecutionResult,
|
|
@@ -107,6 +108,18 @@ export class CommandExecutor {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
try {
|
|
111
|
+
// 0. Parse @ agent mentions from task input
|
|
112
|
+
const taskInput = (params.task as string) || (params.description as string) || ''
|
|
113
|
+
const { prjctAgents, claudeSubagents, cleanInput } = promptBuilder.parseAgentMentions(taskInput)
|
|
114
|
+
|
|
115
|
+
// Update params with clean input (without @ mentions)
|
|
116
|
+
if (prjctAgents.length > 0 || claudeSubagents.length > 0) {
|
|
117
|
+
if (params.task) params.task = cleanInput
|
|
118
|
+
if (params.description) params.description = cleanInput
|
|
119
|
+
params._mentionedAgents = prjctAgents
|
|
120
|
+
params._claudeSubagents = claudeSubagents
|
|
121
|
+
}
|
|
122
|
+
|
|
110
123
|
// 1. Load template
|
|
111
124
|
const template = await templateLoader.load(commandName)
|
|
112
125
|
|
|
@@ -195,27 +208,62 @@ export class CommandExecutor {
|
|
|
195
208
|
)
|
|
196
209
|
}
|
|
197
210
|
|
|
198
|
-
//
|
|
211
|
+
// 8.5. Load @ mentioned agents if any
|
|
212
|
+
let primaryAgent = null
|
|
213
|
+
if (prjctAgents.length > 0) {
|
|
214
|
+
const agentRouter = new AgentRouter()
|
|
215
|
+
await agentRouter.initialize(projectPath)
|
|
216
|
+
const loadedAgents = await agentRouter.loadAgentsByMention(prjctAgents)
|
|
217
|
+
|
|
218
|
+
if (loadedAgents.length > 0) {
|
|
219
|
+
primaryAgent = {
|
|
220
|
+
name: loadedAgents[0].name,
|
|
221
|
+
content: loadedAgents[0].content,
|
|
222
|
+
role: undefined,
|
|
223
|
+
skills: [],
|
|
224
|
+
}
|
|
225
|
+
console.log(`🤖 Loading agents: ${loadedAgents.map((a) => `@${a.name}`).join(', ')}`)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 9. Build prompt - use mentioned agent or let Claude decide via templates
|
|
199
230
|
const planInfo = {
|
|
200
231
|
isPlanning: requiresPlanning || isInPlanningMode,
|
|
201
232
|
requiresApproval: isDestructive && !params.approved,
|
|
202
233
|
active: activePlan,
|
|
203
234
|
allowedTools: planMode.getAllowedTools(isInPlanningMode, template.frontmatter['allowed-tools'] || []),
|
|
204
235
|
}
|
|
205
|
-
|
|
206
|
-
|
|
236
|
+
|
|
237
|
+
// Build base prompt
|
|
238
|
+
let prompt = promptBuilder.build(
|
|
207
239
|
template,
|
|
208
240
|
context as Parameters<typeof promptBuilder.build>[1],
|
|
209
241
|
state,
|
|
210
|
-
|
|
242
|
+
primaryAgent,
|
|
211
243
|
learnedPatterns,
|
|
212
244
|
null,
|
|
213
245
|
relevantMemories,
|
|
214
246
|
planInfo
|
|
215
247
|
)
|
|
216
248
|
|
|
249
|
+
// Add Claude subagent instructions if @ mentioned
|
|
250
|
+
if (claudeSubagents.length > 0) {
|
|
251
|
+
const subagentInstructions = promptBuilder.buildSubagentInstructions(claudeSubagents)
|
|
252
|
+
prompt = prompt + subagentInstructions
|
|
253
|
+
console.log(`🔧 Claude subagents requested: ${claudeSubagents.map((s) => `@${s}`).join(', ')}`)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Add tool permissions from template if present
|
|
257
|
+
const toolPermissions = template.frontmatter['tool-permissions'] as Record<string, { allow?: string[]; ask?: string[]; deny?: string[] }> | undefined
|
|
258
|
+
if (toolPermissions) {
|
|
259
|
+
const permissionsSection = promptBuilder.buildToolPermissions(toolPermissions)
|
|
260
|
+
prompt = prompt + permissionsSection
|
|
261
|
+
}
|
|
262
|
+
|
|
217
263
|
// Log agentic mode
|
|
218
|
-
|
|
264
|
+
if (!primaryAgent) {
|
|
265
|
+
console.log(`🤖 Agentic delegation enabled - Claude will assign agent via Task tool`)
|
|
266
|
+
}
|
|
219
267
|
|
|
220
268
|
// Record successful attempt
|
|
221
269
|
loopDetector.recordSuccess(commandName, loopContext)
|
|
@@ -498,6 +498,87 @@ class PromptBuilder {
|
|
|
498
498
|
return result || null
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Claude Code subagents that can be invoked via @ notation
|
|
503
|
+
*/
|
|
504
|
+
private readonly CLAUDE_SUBAGENTS = ['explore', 'general', 'plan']
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Parse @ agent mentions from input
|
|
508
|
+
* Separates prjct agents from Claude Code subagents
|
|
509
|
+
* @example "@frontend @explore add dark mode" -> { prjctAgents: ["frontend"], claudeSubagents: ["explore"], cleanInput: "add dark mode" }
|
|
510
|
+
*/
|
|
511
|
+
parseAgentMentions(input: string): {
|
|
512
|
+
prjctAgents: string[]
|
|
513
|
+
claudeSubagents: string[]
|
|
514
|
+
cleanInput: string
|
|
515
|
+
} {
|
|
516
|
+
if (!input) return { prjctAgents: [], claudeSubagents: [], cleanInput: '' }
|
|
517
|
+
|
|
518
|
+
const mentionPattern = /@(\w+)/g
|
|
519
|
+
const prjctAgents: string[] = []
|
|
520
|
+
const claudeSubagents: string[] = []
|
|
521
|
+
let match
|
|
522
|
+
|
|
523
|
+
while ((match = mentionPattern.exec(input)) !== null) {
|
|
524
|
+
const name = match[1].toLowerCase()
|
|
525
|
+
if (this.CLAUDE_SUBAGENTS.includes(name)) {
|
|
526
|
+
claudeSubagents.push(name)
|
|
527
|
+
} else {
|
|
528
|
+
prjctAgents.push(name)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const cleanInput = input.replace(mentionPattern, '').trim()
|
|
533
|
+
return { prjctAgents, claudeSubagents, cleanInput }
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Build Claude subagent instructions for the prompt
|
|
538
|
+
*/
|
|
539
|
+
buildSubagentInstructions(claudeSubagents: string[]): string {
|
|
540
|
+
if (!claudeSubagents.length) return ''
|
|
541
|
+
|
|
542
|
+
const instructions = claudeSubagents.map((sub) => {
|
|
543
|
+
switch (sub) {
|
|
544
|
+
case 'explore':
|
|
545
|
+
return '- USE Task tool with subagent_type=Explore for fast codebase exploration'
|
|
546
|
+
case 'general':
|
|
547
|
+
return '- USE Task tool with subagent_type=general-purpose for complex multi-step research'
|
|
548
|
+
case 'plan':
|
|
549
|
+
return '- USE Task tool with subagent_type=Plan for architecture design'
|
|
550
|
+
default:
|
|
551
|
+
return null
|
|
552
|
+
}
|
|
553
|
+
}).filter(Boolean)
|
|
554
|
+
|
|
555
|
+
if (!instructions.length) return ''
|
|
556
|
+
|
|
557
|
+
return `
|
|
558
|
+
## CLAUDE SUBAGENT INSTRUCTIONS
|
|
559
|
+
${instructions.join('\n')}
|
|
560
|
+
`
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Build tool permissions section from template frontmatter
|
|
565
|
+
*/
|
|
566
|
+
buildToolPermissions(toolPermissions: Record<string, { allow?: string[]; ask?: string[]; deny?: string[] }> | null): string {
|
|
567
|
+
if (!toolPermissions) return ''
|
|
568
|
+
|
|
569
|
+
const parts: string[] = ['\n## TOOL PERMISSIONS\n', 'Check these BEFORE executing:\n']
|
|
570
|
+
|
|
571
|
+
for (const [tool, rules] of Object.entries(toolPermissions)) {
|
|
572
|
+
parts.push(`\n**${tool}:**`)
|
|
573
|
+
if (rules.allow?.length) parts.push(`- ALLOW: ${rules.allow.join(', ')}`)
|
|
574
|
+
if (rules.ask?.length) parts.push(`- ASK USER: ${rules.ask.join(', ')}`)
|
|
575
|
+
if (rules.deny?.length) parts.push(`- NEVER: ${rules.deny.join(', ')}`)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
parts.push('\n⚠️ DENY = Never execute. ASK = Confirm with user first.\n')
|
|
579
|
+
return parts.join('\n')
|
|
580
|
+
}
|
|
581
|
+
|
|
501
582
|
/**
|
|
502
583
|
* Build critical anti-hallucination rules section
|
|
503
584
|
*/
|
|
@@ -15,26 +15,92 @@ import type { Frontmatter, ParsedTemplate } from '../types'
|
|
|
15
15
|
const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates', 'commands')
|
|
16
16
|
const MAX_CACHE_SIZE = 50
|
|
17
17
|
|
|
18
|
+
// Use single Map for O(1) LRU operations (ES6 Maps maintain insertion order)
|
|
18
19
|
const cache = new Map<string, ParsedTemplate>()
|
|
19
|
-
const cacheOrder: string[] = []
|
|
20
20
|
|
|
21
21
|
// ============ Cache Helpers ============
|
|
22
22
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
23
|
+
function setWithLru(key: string, value: ParsedTemplate): void {
|
|
24
|
+
// Delete first to move to end when re-adding (most recently used)
|
|
25
|
+
cache.delete(key)
|
|
26
|
+
cache.set(key, value)
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const oldest =
|
|
28
|
+
// Evict oldest (first item) if over limit
|
|
29
|
+
if (cache.size > MAX_CACHE_SIZE) {
|
|
30
|
+
const oldest = cache.keys().next().value
|
|
32
31
|
if (oldest) cache.delete(oldest)
|
|
33
32
|
}
|
|
34
33
|
}
|
|
35
34
|
|
|
35
|
+
function getWithLru(key: string): ParsedTemplate | undefined {
|
|
36
|
+
const value = cache.get(key)
|
|
37
|
+
if (value !== undefined) {
|
|
38
|
+
// Move to end (most recently used)
|
|
39
|
+
cache.delete(key)
|
|
40
|
+
cache.set(key, value)
|
|
41
|
+
}
|
|
42
|
+
return value
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
// ============ Parsing Functions ============
|
|
37
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Parse tool-permissions YAML block
|
|
49
|
+
* Handles nested structure like:
|
|
50
|
+
* tool-permissions:
|
|
51
|
+
* bash:
|
|
52
|
+
* allow: ["git *"]
|
|
53
|
+
* deny: ["rm -rf"]
|
|
54
|
+
*/
|
|
55
|
+
function parseToolPermissions(lines: string[], startIndex: number): {
|
|
56
|
+
permissions: Record<string, { allow?: string[]; ask?: string[]; deny?: string[] }>
|
|
57
|
+
endIndex: number
|
|
58
|
+
} {
|
|
59
|
+
const permissions: Record<string, { allow?: string[]; ask?: string[]; deny?: string[] }> = {}
|
|
60
|
+
let i = startIndex
|
|
61
|
+
let currentTool: string | null = null
|
|
62
|
+
|
|
63
|
+
while (i < lines.length) {
|
|
64
|
+
const line = lines[i]
|
|
65
|
+
const trimmed = line.trim()
|
|
66
|
+
|
|
67
|
+
// End if we hit a new top-level key (no leading spaces)
|
|
68
|
+
if (line.length > 0 && !line.startsWith(' ') && !line.startsWith('\t')) {
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Tool name (2 spaces indent)
|
|
73
|
+
const toolMatch = line.match(/^ {2}(\w+):$/)
|
|
74
|
+
if (toolMatch) {
|
|
75
|
+
currentTool = toolMatch[1]
|
|
76
|
+
permissions[currentTool] = {}
|
|
77
|
+
i++
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Permission array (4 spaces indent)
|
|
82
|
+
const permMatch = line.match(/^ {4}(allow|ask|deny):\s*\[(.+)\]/)
|
|
83
|
+
if (permMatch && currentTool) {
|
|
84
|
+
const [, permType, arrayContent] = permMatch
|
|
85
|
+
permissions[currentTool][permType as 'allow' | 'ask' | 'deny'] = arrayContent
|
|
86
|
+
.split(',')
|
|
87
|
+
.map((v) => v.trim().replace(/^["']|["']$/g, ''))
|
|
88
|
+
i++
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Skip empty lines within block
|
|
93
|
+
if (trimmed === '') {
|
|
94
|
+
i++
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
i++
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { permissions, endIndex: i }
|
|
102
|
+
}
|
|
103
|
+
|
|
38
104
|
export function parseFrontmatter(content: string): ParsedTemplate {
|
|
39
105
|
const frontmatterRegex = /^---\n([\s\S]+?)\n---\n([\s\S]*)$/
|
|
40
106
|
const match = content.match(frontmatterRegex)
|
|
@@ -45,21 +111,35 @@ export function parseFrontmatter(content: string): ParsedTemplate {
|
|
|
45
111
|
|
|
46
112
|
const [, frontmatterText, mainContent] = match
|
|
47
113
|
const frontmatter: Frontmatter = {}
|
|
114
|
+
const lines = frontmatterText.split('\n')
|
|
48
115
|
|
|
49
|
-
|
|
116
|
+
for (let i = 0; i < lines.length; i++) {
|
|
117
|
+
const line = lines[i]
|
|
50
118
|
const [key, ...valueParts] = line.split(':')
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Parse arrays
|
|
55
|
-
if (value.startsWith('[') && value.endsWith(']')) {
|
|
56
|
-
frontmatter[key.trim()] = value.slice(1, -1).split(',').map((v) => v.trim())
|
|
57
|
-
} else {
|
|
58
|
-
// Remove quotes if present
|
|
59
|
-
frontmatter[key.trim()] = value.replace(/^["']|["']$/g, '')
|
|
60
|
-
}
|
|
119
|
+
|
|
120
|
+
if (!key || line.startsWith(' ') || line.startsWith('\t')) {
|
|
121
|
+
continue
|
|
61
122
|
}
|
|
62
|
-
|
|
123
|
+
|
|
124
|
+
const keyTrimmed = key.trim()
|
|
125
|
+
const value = valueParts.join(':').trim()
|
|
126
|
+
|
|
127
|
+
// Handle tool-permissions nested block
|
|
128
|
+
if (keyTrimmed === 'tool-permissions' && value === '') {
|
|
129
|
+
const { permissions, endIndex } = parseToolPermissions(lines, i + 1)
|
|
130
|
+
frontmatter['tool-permissions'] = permissions
|
|
131
|
+
i = endIndex - 1
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Parse arrays
|
|
136
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
137
|
+
frontmatter[keyTrimmed] = value.slice(1, -1).split(',').map((v) => v.trim())
|
|
138
|
+
} else if (value) {
|
|
139
|
+
// Remove quotes if present
|
|
140
|
+
frontmatter[keyTrimmed] = value.replace(/^["']|["']$/g, '')
|
|
141
|
+
}
|
|
142
|
+
}
|
|
63
143
|
|
|
64
144
|
return { frontmatter, content: mainContent.trim() }
|
|
65
145
|
}
|
|
@@ -67,10 +147,10 @@ export function parseFrontmatter(content: string): ParsedTemplate {
|
|
|
67
147
|
// ============ Main Functions ============
|
|
68
148
|
|
|
69
149
|
export async function load(commandName: string): Promise<ParsedTemplate> {
|
|
70
|
-
// Check cache first
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return
|
|
150
|
+
// Check cache first with LRU update
|
|
151
|
+
const cached = getWithLru(commandName)
|
|
152
|
+
if (cached) {
|
|
153
|
+
return cached
|
|
74
154
|
}
|
|
75
155
|
|
|
76
156
|
const templatePath = path.join(TEMPLATES_DIR, `${commandName}.md`)
|
|
@@ -79,12 +159,8 @@ export async function load(commandName: string): Promise<ParsedTemplate> {
|
|
|
79
159
|
const rawContent = await fs.readFile(templatePath, 'utf-8')
|
|
80
160
|
const parsed = parseFrontmatter(rawContent)
|
|
81
161
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Cache result
|
|
86
|
-
cache.set(commandName, parsed)
|
|
87
|
-
cacheOrder.push(commandName)
|
|
162
|
+
// Cache with LRU management
|
|
163
|
+
setWithLru(commandName, parsed)
|
|
88
164
|
|
|
89
165
|
return parsed
|
|
90
166
|
} catch {
|
|
@@ -99,7 +175,6 @@ export async function getAllowedTools(commandName: string): Promise<string[]> {
|
|
|
99
175
|
|
|
100
176
|
export function clearCache(): void {
|
|
101
177
|
cache.clear()
|
|
102
|
-
cacheOrder.length = 0
|
|
103
178
|
}
|
|
104
179
|
|
|
105
180
|
// ============ Default Export (backwards compat) ============
|
|
@@ -70,23 +70,21 @@ class AgentLoader {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* Load all agents for the project
|
|
73
|
+
* Load all agents for the project (parallel loading for performance)
|
|
74
74
|
*/
|
|
75
75
|
async loadAllAgents(): Promise<Agent[]> {
|
|
76
76
|
try {
|
|
77
77
|
const files = await fs.readdir(this.agentsDir)
|
|
78
78
|
const agentFiles = files.filter((f) => f.endsWith('.md') && !f.startsWith('.'))
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// Load all agents in parallel for better performance
|
|
81
|
+
const agentPromises = agentFiles.map((file) => {
|
|
82
82
|
const agentName = file.replace('.md', '')
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
agents.push(agent)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
83
|
+
return this.loadAgent(agentName)
|
|
84
|
+
})
|
|
88
85
|
|
|
89
|
-
|
|
86
|
+
const results = await Promise.all(agentPromises)
|
|
87
|
+
return results.filter((agent): agent is Agent => agent !== null)
|
|
90
88
|
} catch (error) {
|
|
91
89
|
if (isNotFoundError(error)) {
|
|
92
90
|
return [] // Agents directory doesn't exist yet
|
|
@@ -137,21 +137,21 @@ class ContextEstimator {
|
|
|
137
137
|
globPatterns.push(`*${ext}`)
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
// Execute glob searches
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
140
|
+
// Execute glob searches in parallel for better performance
|
|
141
|
+
const ignorePatterns = patterns.exclude.map((ex) => `**/${ex}/**`)
|
|
142
|
+
const globPromises = globPatterns.map((pattern) =>
|
|
143
|
+
glob(pattern, {
|
|
144
|
+
cwd: projectPath,
|
|
145
|
+
ignore: ignorePatterns,
|
|
146
|
+
nodir: true,
|
|
147
|
+
follow: false,
|
|
148
|
+
}).catch(() => [] as string[]) // Return empty array on error
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const results = await Promise.all(globPromises)
|
|
152
|
+
for (const matches of results) {
|
|
153
|
+
if (Array.isArray(matches)) {
|
|
154
|
+
files.push(...matches)
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -42,6 +42,10 @@ function parseJsonc<T>(content: string): T {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
class ConfigManager {
|
|
45
|
+
// Cache projectId lookups with TTL (30 seconds) to avoid repeated disk reads
|
|
46
|
+
private projectIdCache = new Map<string, { id: string; timestamp: number }>()
|
|
47
|
+
private readonly CACHE_TTL = 30000
|
|
48
|
+
|
|
45
49
|
/**
|
|
46
50
|
* Read the project configuration file
|
|
47
51
|
* Supports both .json and .jsonc formats (with comments)
|
|
@@ -228,13 +232,30 @@ class ConfigManager {
|
|
|
228
232
|
|
|
229
233
|
/**
|
|
230
234
|
* Get the project ID from config, or generate it if config doesn't exist
|
|
235
|
+
* Uses in-memory cache with TTL to avoid repeated disk reads
|
|
231
236
|
*/
|
|
232
237
|
async getProjectId(projectPath: string): Promise<string> {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
238
|
+
// Check cache first
|
|
239
|
+
const cached = this.projectIdCache.get(projectPath)
|
|
240
|
+
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
|
|
241
|
+
return cached.id
|
|
236
242
|
}
|
|
237
|
-
|
|
243
|
+
|
|
244
|
+
// Read from disk
|
|
245
|
+
const config = await this.readConfig(projectPath)
|
|
246
|
+
const id = config?.projectId || pathManager.generateProjectId(projectPath)
|
|
247
|
+
|
|
248
|
+
// Cache the result
|
|
249
|
+
this.projectIdCache.set(projectPath, { id, timestamp: Date.now() })
|
|
250
|
+
|
|
251
|
+
return id
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Clear the projectId cache (useful for testing or after config changes)
|
|
256
|
+
*/
|
|
257
|
+
clearProjectIdCache(): void {
|
|
258
|
+
this.projectIdCache.clear()
|
|
238
259
|
}
|
|
239
260
|
|
|
240
261
|
/**
|