prjct-cli 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/bin/dev.js +0 -1
  3. package/bin/serve.js +19 -20
  4. package/core/agentic/agent-router.ts +79 -14
  5. package/core/agentic/command-executor/command-executor.ts +2 -74
  6. package/core/agentic/services.ts +0 -48
  7. package/core/agentic/template-loader.ts +35 -1
  8. package/core/commands/base.ts +96 -77
  9. package/core/commands/planning.ts +13 -2
  10. package/core/commands/setup.ts +3 -85
  11. package/core/errors.ts +209 -0
  12. package/core/infrastructure/config-manager.ts +22 -5
  13. package/core/infrastructure/setup.ts +5 -50
  14. package/core/storage/storage-manager.ts +42 -6
  15. package/core/utils/logger.ts +19 -12
  16. package/package.json +2 -4
  17. package/templates/agentic/subagent-generation.md +109 -0
  18. package/templates/commands/setup.md +18 -3
  19. package/templates/commands/sync.md +74 -13
  20. package/templates/mcp-config.json +20 -1
  21. package/templates/subagents/domain/backend.md +105 -0
  22. package/templates/subagents/domain/database.md +118 -0
  23. package/templates/subagents/domain/devops.md +148 -0
  24. package/templates/subagents/domain/frontend.md +99 -0
  25. package/templates/subagents/domain/testing.md +169 -0
  26. package/templates/subagents/workflow/prjct-planner.md +158 -0
  27. package/templates/subagents/workflow/prjct-shipper.md +179 -0
  28. package/templates/subagents/workflow/prjct-workflow.md +98 -0
  29. package/bin/generate-views.js +0 -209
  30. package/bin/migrate-to-json.js +0 -742
  31. package/core/agentic/context-filter.ts +0 -365
  32. package/core/agentic/parallel-tools.ts +0 -165
  33. package/core/agentic/response-templates.ts +0 -164
  34. package/core/agentic/semantic-compression.ts +0 -273
  35. package/core/agentic/think-blocks.ts +0 -202
  36. package/core/agentic/validation-rules.ts +0 -313
  37. package/core/domain/agent-matcher.ts +0 -130
  38. package/core/domain/agent-validator.ts +0 -250
  39. package/core/domain/architect-session.ts +0 -315
  40. package/core/domain/product-standards.ts +0 -106
  41. package/core/domain/smart-cache.ts +0 -167
  42. package/core/domain/task-analyzer.ts +0 -296
  43. package/core/infrastructure/legacy-installer-detector/cleanup.ts +0 -216
  44. package/core/infrastructure/legacy-installer-detector/detection.ts +0 -95
  45. package/core/infrastructure/legacy-installer-detector/index.ts +0 -171
  46. package/core/infrastructure/legacy-installer-detector/migration.ts +0 -87
  47. package/core/infrastructure/legacy-installer-detector/types.ts +0 -42
  48. package/core/infrastructure/legacy-installer-detector.ts +0 -7
  49. package/core/infrastructure/migrator/file-operations.ts +0 -125
  50. package/core/infrastructure/migrator/index.ts +0 -288
  51. package/core/infrastructure/migrator/project-scanner.ts +0 -90
  52. package/core/infrastructure/migrator/reports.ts +0 -117
  53. package/core/infrastructure/migrator/types.ts +0 -124
  54. package/core/infrastructure/migrator/validation.ts +0 -94
  55. package/core/infrastructure/migrator/version-migration.ts +0 -117
  56. package/core/infrastructure/migrator.ts +0 -10
  57. package/core/infrastructure/uuid-migration.ts +0 -750
  58. package/templates/commands/migrate-all.md +0 -96
  59. package/templates/commands/migrate.md +0 -140
package/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.16.0] - 2025-12-15
4
+
5
+ ### Dead Code Cleanup
6
+
7
+ Aggressive removal of ~6,600 lines of unused/legacy code for a cleaner, leaner codebase.
8
+
9
+ - **Domain Modules Removed (6 files)**
10
+ - `agent-matcher.ts` - Replaced by template-based routing
11
+ - `product-standards.ts` - Unused domain standards
12
+ - `smart-cache.ts`, `task-analyzer.ts`, `agent-validator.ts`, `architect-session.ts`
13
+
14
+ - **Agentic Modules Removed (6 files)**
15
+ - `semantic-compression.ts` - Exported but never called
16
+ - `response-templates.ts`, `think-blocks.ts`, `parallel-tools.ts`
17
+ - `context-filter.ts`, `validation-rules.ts`
18
+
19
+ - **Legacy Migration Code Removed**
20
+ - `legacy-installer-detector/` directory (curl-to-npm migration)
21
+ - `migrator/` directory (version migrations)
22
+ - `uuid-migration.ts` (750 lines)
23
+ - `bin/migrate-to-json.js`, `bin/generate-views.js`
24
+ - `/p:migrate`, `/p:migrate-all` command templates
25
+
26
+ - **Code Cleanup**
27
+ - Simplified `services.ts` facade (removed 6 unused exports)
28
+ - Simplified `setup.ts` (removed migration steps)
29
+ - Removed deprecated methods from `base.ts`
30
+ - Cleaned `command-executor.ts` (removed dead module usage)
31
+
32
+ - **Impact**
33
+ - Lines removed: ~6,600
34
+ - Files deleted: ~25
35
+ - Build: Passing
36
+ - Tests: 131 pass
37
+
38
+ ## [0.15.1] - 2025-12-15
39
+
40
+ ### MCP Setup Documentation
41
+
42
+ Enhanced setup template with MCP server installation documentation.
43
+
44
+ - **Setup Template Updates**
45
+ - Added MCP server installation step documentation
46
+ - Context7 library documentation lookup integration
47
+ - Updated success output examples
48
+
49
+ - **MCP Config Enhancements**
50
+ - Added description field for Context7 server
51
+ - Added usage guidelines with when/tools/examples
52
+ - Better documentation for resolve-library-id and get-library-docs
53
+
3
54
  ## [0.15.0] - 2025-12-15
4
55
 
5
56
  ### Template Optimization - Aggressive Consolidation
package/bin/dev.js CHANGED
@@ -153,7 +153,6 @@ async function main() {
153
153
  cwd: WEB_PATH,
154
154
  env: { ...process.env, PORT: PORT.toString() },
155
155
  stdio: ['ignore', 'pipe', 'pipe'],
156
- shell: true
157
156
  })
158
157
 
159
158
  webProc.stdout.on('data', (data) => {
package/bin/serve.js CHANGED
@@ -138,7 +138,6 @@ if (sharedCheck.needed || webCheck.needed) {
138
138
  const sharedInstall = spawnSync('npm', ['install'], {
139
139
  cwd: sharedDir,
140
140
  stdio: 'inherit',
141
- shell: true,
142
141
  })
143
142
 
144
143
  if (sharedInstall.status !== 0) {
@@ -151,7 +150,6 @@ if (sharedCheck.needed || webCheck.needed) {
151
150
  const sharedBuild = spawnSync('npm', ['run', 'build'], {
152
151
  cwd: sharedDir,
153
152
  stdio: 'inherit',
154
- shell: true,
155
153
  })
156
154
 
157
155
  if (sharedBuild.status !== 0) {
@@ -167,7 +165,6 @@ if (sharedCheck.needed || webCheck.needed) {
167
165
  const webInstall = spawnSync('npm', ['install'], {
168
166
  cwd: webDir,
169
167
  stdio: 'inherit',
170
- shell: true,
171
168
  })
172
169
 
173
170
  if (webInstall.status !== 0) {
@@ -189,6 +186,7 @@ if (sharedCheck.needed || webCheck.needed) {
189
186
  function isPrjctWebProcess(pid) {
190
187
  try {
191
188
  if (process.platform === 'win32') {
189
+ // Windows: wmic needs shell for proper command parsing
192
190
  const result = spawnSync('wmic', ['process', 'where', `processid=${pid}`, 'get', 'commandline'], {
193
191
  shell: true,
194
192
  encoding: 'utf8',
@@ -197,10 +195,9 @@ function isPrjctWebProcess(pid) {
197
195
  } else {
198
196
  // macOS / Linux - check process command line
199
197
  const result = spawnSync('ps', ['-p', pid, '-o', 'command='], {
200
- shell: true,
201
198
  encoding: 'utf8',
202
199
  })
203
- const cmd = result.stdout.trim()
200
+ const cmd = result.stdout?.trim() || ''
204
201
  return cmd.includes('server.ts') || cmd.includes('prjct') || cmd.includes('next')
205
202
  }
206
203
  } catch {
@@ -214,9 +211,10 @@ function isPrjctWebProcess(pid) {
214
211
  function getPortPids(portToCheck) {
215
212
  try {
216
213
  if (process.platform === 'win32') {
217
- const result = spawnSync('netstat', ['-ano'], { shell: true, encoding: 'utf8' })
214
+ // Windows: netstat works without shell
215
+ const result = spawnSync('netstat', ['-ano'], { encoding: 'utf8' })
218
216
  const pids = []
219
- const lines = result.stdout.split('\n')
217
+ const lines = (result.stdout || '').split('\n')
220
218
  for (const line of lines) {
221
219
  if (line.includes(`:${portToCheck}`) && line.includes('LISTENING')) {
222
220
  const parts = line.trim().split(/\s+/)
@@ -226,11 +224,11 @@ function getPortPids(portToCheck) {
226
224
  }
227
225
  return pids
228
226
  } else {
227
+ // macOS / Linux
229
228
  const result = spawnSync('lsof', ['-ti', `:${portToCheck}`], {
230
- shell: true,
231
229
  encoding: 'utf8',
232
230
  })
233
- return result.stdout.trim().split('\n').filter(Boolean)
231
+ return (result.stdout || '').trim().split('\n').filter(Boolean)
234
232
  }
235
233
  } catch {
236
234
  return []
@@ -244,9 +242,11 @@ function killPids(pids) {
244
242
  for (const pid of pids) {
245
243
  try {
246
244
  if (process.platform === 'win32') {
247
- spawnSync('taskkill', ['/F', '/PID', pid], { shell: true })
245
+ // Windows: taskkill works without shell
246
+ spawnSync('taskkill', ['/F', '/PID', pid])
248
247
  } else {
249
- spawnSync('kill', ['-9', pid], { shell: true })
248
+ // Unix: kill works without shell
249
+ spawnSync('kill', ['-9', pid])
250
250
  }
251
251
  } catch {
252
252
  // Ignore individual kill errors
@@ -258,13 +258,14 @@ function killPids(pids) {
258
258
  * Open URL in browser
259
259
  */
260
260
  function openBrowser(url) {
261
- const openCmd =
262
- process.platform === 'darwin'
263
- ? 'open'
264
- : process.platform === 'win32'
265
- ? 'start'
266
- : 'xdg-open'
267
- spawn(openCmd, [url], { shell: true, detached: true }).unref()
261
+ if (process.platform === 'darwin') {
262
+ spawn('open', [url], { detached: true, stdio: 'ignore' }).unref()
263
+ } else if (process.platform === 'win32') {
264
+ // Windows 'start' is a shell builtin, requires shell: true
265
+ spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref()
266
+ } else {
267
+ spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref()
268
+ }
268
269
  }
269
270
 
270
271
  // Check if port is in use and handle accordingly
@@ -324,7 +325,6 @@ if (!fs.existsSync(nextDir)) {
324
325
  const buildResult = spawnSync('npm', ['run', 'build'], {
325
326
  cwd: webDir,
326
327
  stdio: 'inherit',
327
- shell: true,
328
328
  })
329
329
  if (buildResult.status !== 0) {
330
330
  console.error('❌ Build failed')
@@ -337,7 +337,6 @@ if (!fs.existsSync(nextDir)) {
337
337
  const web = spawn('npm', ['run', 'start'], {
338
338
  cwd: webDir,
339
339
  stdio: 'inherit',
340
- shell: true,
341
340
  env: { ...process.env, PORT: port, NODE_ENV: 'production' },
342
341
  })
343
342
 
@@ -2,8 +2,14 @@
2
2
  * Agent Router
3
3
  * Orchestrates agent loading and context building for Claude delegation.
4
4
  *
5
+ * Loads agents from two locations:
6
+ * 1. {projectPath}/.claude/agents/ - Claude Code sub-agents (PRIMARY, per-project)
7
+ * 2. ~/.prjct-cli/projects/{id}/agents/ - Legacy agents (fallback)
8
+ *
9
+ * Claude Code sub-agents are prioritized as they're more specific to the project.
10
+ *
5
11
  * @module agentic/agent-router
6
- * @version 2.0.0
12
+ * @version 3.0.0
7
13
  */
8
14
 
9
15
  import fs from 'fs/promises'
@@ -14,6 +20,7 @@ import pathManager from '../infrastructure/path-manager'
14
20
  interface Agent {
15
21
  name: string
16
22
  content: string
23
+ source: 'claude-code' | 'legacy'
17
24
  }
18
25
 
19
26
  interface AssignmentContext {
@@ -30,29 +37,42 @@ interface AssignmentContext {
30
37
  */
31
38
  class AgentRouter {
32
39
  projectId: string | null = null
33
- agentsPath: string | null = null
40
+ projectPath: string | null = null
41
+ legacyAgentsPath: string | null = null
42
+
43
+ /**
44
+ * Get path to Claude Code sub-agents directory
45
+ */
46
+ getClaudeCodeAgentsPath(): string | null {
47
+ if (!this.projectPath) return null
48
+ return path.join(this.projectPath, '.claude', 'agents')
49
+ }
34
50
 
35
51
  /**
36
52
  * Initialize router with project context
37
53
  */
38
54
  async initialize(projectPath: string): Promise<void> {
39
55
  this.projectId = await configManager.getProjectId(projectPath)
40
- this.agentsPath = pathManager.getPath(this.projectId!, 'agents')
56
+ this.projectPath = projectPath
57
+ this.legacyAgentsPath = pathManager.getPath(this.projectId!, 'agents')
41
58
  }
42
59
 
43
60
  /**
44
- * Load all available agents from project
61
+ * Load agents from a specific directory
45
62
  */
46
- async loadAvailableAgents(): Promise<Agent[]> {
63
+ private async loadAgentsFromPath(
64
+ agentsPath: string,
65
+ source: 'claude-code' | 'legacy'
66
+ ): Promise<Agent[]> {
47
67
  try {
48
- const files = await fs.readdir(this.agentsPath!)
68
+ const files = await fs.readdir(agentsPath)
49
69
  const agents: Agent[] = []
50
70
 
51
71
  for (const file of files) {
52
72
  if (file.endsWith('.md')) {
53
73
  const name = file.replace('.md', '')
54
- const content = await fs.readFile(path.join(this.agentsPath!, file), 'utf-8')
55
- agents.push({ name, content })
74
+ const content = await fs.readFile(path.join(agentsPath, file), 'utf-8')
75
+ agents.push({ name, content, source })
56
76
  }
57
77
  }
58
78
 
@@ -62,6 +82,33 @@ class AgentRouter {
62
82
  }
63
83
  }
64
84
 
85
+ /**
86
+ * Load all available agents from both Claude Code and legacy locations
87
+ * Claude Code sub-agents take priority over legacy agents with same name
88
+ */
89
+ async loadAvailableAgents(): Promise<Agent[]> {
90
+ const agentMap = new Map<string, Agent>()
91
+
92
+ // Load legacy agents first (lower priority)
93
+ if (this.legacyAgentsPath) {
94
+ const legacyAgents = await this.loadAgentsFromPath(this.legacyAgentsPath, 'legacy')
95
+ for (const agent of legacyAgents) {
96
+ agentMap.set(agent.name, agent)
97
+ }
98
+ }
99
+
100
+ // Load Claude Code sub-agents (higher priority - overwrites legacy)
101
+ const claudeCodePath = this.getClaudeCodeAgentsPath()
102
+ if (claudeCodePath) {
103
+ const claudeCodeAgents = await this.loadAgentsFromPath(claudeCodePath, 'claude-code')
104
+ for (const agent of claudeCodeAgents) {
105
+ agentMap.set(agent.name, agent)
106
+ }
107
+ }
108
+
109
+ return Array.from(agentMap.values())
110
+ }
111
+
65
112
  /**
66
113
  * Get list of available agent names
67
114
  */
@@ -72,15 +119,33 @@ class AgentRouter {
72
119
 
73
120
  /**
74
121
  * Load a specific agent by name
122
+ * Checks Claude Code sub-agents first, then falls back to legacy
75
123
  */
76
124
  async loadAgent(name: string): Promise<Agent | null> {
77
- try {
78
- const filePath = path.join(this.agentsPath!, `${name}.md`)
79
- const content = await fs.readFile(filePath, 'utf-8')
80
- return { name, content }
81
- } catch {
82
- return null
125
+ // Try Claude Code sub-agents first (higher priority)
126
+ const claudeCodePath = this.getClaudeCodeAgentsPath()
127
+ if (claudeCodePath) {
128
+ try {
129
+ const filePath = path.join(claudeCodePath, `${name}.md`)
130
+ const content = await fs.readFile(filePath, 'utf-8')
131
+ return { name, content, source: 'claude-code' }
132
+ } catch {
133
+ // Not found in Claude Code path, try legacy
134
+ }
135
+ }
136
+
137
+ // Fall back to legacy agents
138
+ if (this.legacyAgentsPath) {
139
+ try {
140
+ const filePath = path.join(this.legacyAgentsPath, `${name}.md`)
141
+ const content = await fs.readFile(filePath, 'utf-8')
142
+ return { name, content, source: 'legacy' }
143
+ } catch {
144
+ // Not found
145
+ }
83
146
  }
147
+
148
+ return null
84
149
  }
85
150
 
86
151
  /**
@@ -9,15 +9,10 @@ import templateLoader from '../template-loader'
9
9
  import contextBuilder from '../context-builder'
10
10
  import promptBuilder from '../prompt-builder'
11
11
  import toolRegistry from '../tool-registry'
12
- import { validate, formatError } from '../validation-rules'
13
12
  import loopDetector from '../loop-detector'
14
13
  import chainOfThought from '../chain-of-thought'
15
- import semanticCompression from '../semantic-compression'
16
- import responseTemplates from '../response-templates'
17
14
  import memorySystem from '../memory-system'
18
15
  import groundTruth from '../ground-truth'
19
- import thinkBlocks from '../think-blocks'
20
- import parallelTools from '../parallel-tools'
21
16
  import planMode from '../plan-mode'
22
17
  import { signalStart, signalEnd } from './status-signal'
23
18
  import type { ExecutionResult, SimpleExecutionResult, ExecutionToolsFn } from './types'
@@ -71,18 +66,6 @@ export class CommandExecutor {
71
66
  // 2. Build METADATA context only (lazy loading - no file reads yet)
72
67
  const metadataContext = await contextBuilder.build(projectPath, params)
73
68
 
74
- // 2.5. VALIDATE: Pre-flight checks with specific errors
75
- const validation = await validate(commandName, metadataContext as unknown as Parameters<typeof validate>[1])
76
- if (!validation.valid) {
77
- this.signalEnd()
78
- return {
79
- success: false,
80
- error: formatError(validation),
81
- validation,
82
- isValidationError: true,
83
- }
84
- }
85
-
86
69
  // 2.55. P3.4 PLAN MODE: Check if command requires planning
87
70
  const requiresPlanning = planMode.requiresPlanning(commandName)
88
71
  const isDestructive = planMode.isDestructive(commandName)
@@ -112,29 +95,6 @@ export class CommandExecutor {
112
95
  }
113
96
  }
114
97
 
115
- // 2.7. THINK BLOCKS (P3.1): Dynamic reasoning based on triggers
116
- let thinkBlock = null
117
- const preThinkState =
118
- groundTruthResult?.actual || (await contextBuilder.loadStateForCommand(metadataContext, commandName))
119
- const thinkTrigger = thinkBlocks.detectTrigger(
120
- commandName,
121
- metadataContext as unknown as Parameters<typeof thinkBlocks.detectTrigger>[1],
122
- preThinkState as Parameters<typeof thinkBlocks.detectTrigger>[2]
123
- )
124
- if (thinkTrigger) {
125
- thinkBlock = await thinkBlocks.generate(
126
- thinkTrigger,
127
- commandName,
128
- metadataContext as unknown as Parameters<typeof thinkBlocks.generate>[2],
129
- preThinkState as Parameters<typeof thinkBlocks.generate>[3]
130
- )
131
-
132
- // Log think block if in debug mode
133
- if (process.env.PRJCT_DEBUG === 'true') {
134
- console.log(thinkBlocks.format(thinkBlock, true))
135
- }
136
- }
137
-
138
98
  // 2.8. CHAIN OF THOUGHT: Reasoning for critical commands
139
99
  let reasoning = null
140
100
  if (chainOfThought.requiresReasoning(commandName)) {
@@ -164,29 +124,7 @@ export class CommandExecutor {
164
124
  }
165
125
 
166
126
  // 6. Load state with filtered context
167
- const rawState = await contextBuilder.loadState(metadataContext)
168
-
169
- // 6.5. SEMANTIC COMPRESSION: Compress state for reduced token usage
170
- const compressedState: Record<string, unknown> = {}
171
- for (const [key, content] of Object.entries(rawState)) {
172
- if (content) {
173
- const compressed = semanticCompression.compress(content, key)
174
- compressedState[key] = {
175
- raw: content,
176
- summary: compressed.summary,
177
- compressed,
178
- }
179
- } else {
180
- compressedState[key] = { raw: null, summary: 'Empty', compressed: null }
181
- }
182
- }
183
-
184
- // Use compressed summaries for prompt, keep raw for tool execution
185
- const state = {
186
- ...rawState,
187
- _compressed: compressedState,
188
- _compressionMetrics: semanticCompression.getMetrics(),
189
- }
127
+ const state = await contextBuilder.loadState(metadataContext)
190
128
 
191
129
  // 7. MEMORY: Load learned patterns AND relevant memories for this command
192
130
  let learnedPatterns = null
@@ -224,7 +162,7 @@ export class CommandExecutor {
224
162
  state,
225
163
  null,
226
164
  learnedPatterns,
227
- thinkBlock,
165
+ null,
228
166
  relevantMemories,
229
167
  planInfo
230
168
  )
@@ -248,19 +186,9 @@ export class CommandExecutor {
248
186
  agentsPath: context.agentsPath as string,
249
187
  agentRoutingPath: context.agentRoutingPath as string,
250
188
  reasoning,
251
- thinkBlock,
252
189
  groundTruth: groundTruthResult,
253
- compressionMetrics: state._compressionMetrics,
254
190
  learnedPatterns,
255
191
  relevantMemories,
256
- formatResponse: (data: unknown) => responseTemplates.format(commandName, data as Parameters<typeof responseTemplates.format>[1]),
257
- formatThinkBlock: (verbose: boolean) => thinkBlocks.format(thinkBlock, verbose),
258
- parallel: {
259
- execute: (toolCalls: unknown[]) => parallelTools.execute(toolCalls as Parameters<typeof parallelTools.execute>[0]),
260
- readAll: (paths: string[]) => parallelTools.readAll(paths),
261
- canParallelize: (tools: string[]) => parallelTools.canParallelize(tools),
262
- getMetrics: () => parallelTools.getMetrics(),
263
- },
264
192
  memory: {
265
193
  create: (memory: unknown) =>
266
194
  memorySystem.createMemory(metadataContext.projectId!, memory as Parameters<typeof memorySystem.createMemory>[1]),
@@ -29,14 +29,8 @@ import toolRegistry from './tool-registry'
29
29
  import loopDetector from './loop-detector'
30
30
  import memorySystem from './memory-system'
31
31
  import groundTruth from './ground-truth'
32
- import semanticCompression from './semantic-compression'
33
- import responseTemplates from './response-templates'
34
32
  import chainOfThought from './chain-of-thought'
35
- import thinkBlocks from './think-blocks'
36
- import parallelTools from './parallel-tools'
37
33
  import planMode from './plan-mode'
38
- import contextFilter from './context-filter'
39
- import validationRules from './validation-rules'
40
34
  import agentRouter from './agent-router'
41
35
  import smartContext from './smart-context'
42
36
  import { mdManagers } from '../data'
@@ -90,54 +84,18 @@ export interface AgenticServices {
90
84
  */
91
85
  truth: typeof groundTruth
92
86
 
93
- /**
94
- * Semantic compression - compresses context to fit token limits
95
- * @see semanticCompression
96
- */
97
- compression: typeof semanticCompression
98
-
99
- /**
100
- * Response templates - generates formatted responses
101
- * @see responseTemplates
102
- */
103
- responses: typeof responseTemplates
104
-
105
87
  /**
106
88
  * Chain of thought - adds reasoning to responses
107
89
  * @see chainOfThought
108
90
  */
109
91
  reasoning: typeof chainOfThought
110
92
 
111
- /**
112
- * Think blocks - generates think block content
113
- * @see thinkBlocks
114
- */
115
- thinking: typeof thinkBlocks
116
-
117
- /**
118
- * Parallel tools - executes tools in parallel
119
- * @see parallelTools
120
- */
121
- parallel: typeof parallelTools
122
-
123
93
  /**
124
94
  * Plan mode - handles planning workflow
125
95
  * @see planMode
126
96
  */
127
97
  planning: typeof planMode
128
98
 
129
- /**
130
- * Context filter - filters context for relevance
131
- * @see contextFilter
132
- */
133
- filter: typeof contextFilter
134
-
135
- /**
136
- * Validation rules - validates command inputs
137
- * @see validationRules
138
- */
139
- validation: typeof validationRules
140
-
141
99
  /**
142
100
  * Agent router - routes tasks to appropriate agents
143
101
  * @see agentRouter
@@ -187,14 +145,8 @@ export const services: AgenticServices = {
187
145
  loops: loopDetector,
188
146
  memory: memorySystem,
189
147
  truth: groundTruth,
190
- compression: semanticCompression,
191
- responses: responseTemplates,
192
148
  reasoning: chainOfThought,
193
- thinking: thinkBlocks,
194
- parallel: parallelTools,
195
149
  planning: planMode,
196
- filter: contextFilter,
197
- validation: validationRules,
198
150
  router: agentRouter,
199
151
  smartContext: smartContext,
200
152
  data: mdManagers,
@@ -8,6 +8,7 @@
8
8
 
9
9
  import fs from 'fs/promises'
10
10
  import path from 'path'
11
+ import { TemplateError } from '../errors'
11
12
 
12
13
  interface Frontmatter {
13
14
  name?: string
@@ -24,14 +25,42 @@ interface ParsedTemplate {
24
25
  /**
25
26
  * Loads command templates from templates/commands/ with caching.
26
27
  * Parses YAML-like frontmatter for metadata extraction.
28
+ * Uses LRU cache with size limit to prevent memory leaks.
27
29
  */
28
30
  class TemplateLoader {
29
31
  templatesDir: string
30
32
  cache: Map<string, ParsedTemplate>
33
+ cacheOrder: string[] // Track access order for LRU eviction
34
+ maxCacheSize: number
31
35
 
32
36
  constructor() {
33
37
  this.templatesDir = path.join(__dirname, '..', '..', 'templates', 'commands')
34
38
  this.cache = new Map()
39
+ this.cacheOrder = []
40
+ this.maxCacheSize = 50 // More than enough for all commands
41
+ }
42
+
43
+ /**
44
+ * Update LRU order - move key to end (most recently used)
45
+ */
46
+ private updateLruOrder(key: string): void {
47
+ const index = this.cacheOrder.indexOf(key)
48
+ if (index > -1) {
49
+ this.cacheOrder.splice(index, 1)
50
+ }
51
+ this.cacheOrder.push(key)
52
+ }
53
+
54
+ /**
55
+ * Evict least recently used entry if cache exceeds max size
56
+ */
57
+ private evictLru(): void {
58
+ while (this.cache.size >= this.maxCacheSize && this.cacheOrder.length > 0) {
59
+ const oldest = this.cacheOrder.shift()
60
+ if (oldest) {
61
+ this.cache.delete(oldest)
62
+ }
63
+ }
35
64
  }
36
65
 
37
66
  /**
@@ -40,6 +69,7 @@ class TemplateLoader {
40
69
  async load(commandName: string): Promise<ParsedTemplate> {
41
70
  // Check cache first
42
71
  if (this.cache.has(commandName)) {
72
+ this.updateLruOrder(commandName)
43
73
  return this.cache.get(commandName)!
44
74
  }
45
75
 
@@ -49,12 +79,16 @@ class TemplateLoader {
49
79
  const rawContent = await fs.readFile(templatePath, 'utf-8')
50
80
  const parsed = this.parseFrontmatter(rawContent)
51
81
 
82
+ // Evict LRU if needed before adding
83
+ this.evictLru()
84
+
52
85
  // Cache result
53
86
  this.cache.set(commandName, parsed)
87
+ this.cacheOrder.push(commandName)
54
88
 
55
89
  return parsed
56
90
  } catch {
57
- throw new Error(`Template not found: ${commandName}.md`)
91
+ throw TemplateError.notFound(commandName)
58
92
  }
59
93
  }
60
94