prjct-cli 0.28.2 → 0.28.3

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 CHANGED
@@ -1,5 +1,67 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.28.3] - 2026-01-11
4
+
5
+ ### Feature: Claude Code Synergy Optimization
6
+
7
+ Major improvements to maximize prjct + Claude Code integration efficiency.
8
+
9
+ **Skill Auto-Invocation (Phase 4 of /p:task):**
10
+ - Skills are now automatically invoked when loading agents
11
+ - Agent frontmatter `skills: [skill-name]` triggers `Skill("skill-name")`
12
+ - Example: Loading `frontend.md` auto-activates `/frontend-design`
13
+
14
+ **MCP Auto-Usage:**
15
+ - Agents with `mcp: [context7]` auto-query documentation during tasks
16
+ - Per-project MCP config at `{globalPath}/config/mcp-servers.json`
17
+ - Seamless library documentation lookup during implementation
18
+
19
+ **Think Blocks for Destructive Commands:**
20
+ - `/p:ship` now includes mandatory `<think>` block
21
+ - Pre-ship verification checklist (completeness, quality, git state)
22
+ - Prevents shipping incomplete or broken code
23
+
24
+ **Agent Auto-Refresh in /p:sync:**
25
+ - Detects when dependencies change (package.json, etc.)
26
+ - Regenerates stale agents (>7 days old)
27
+ - Versions previous agents as `.backup` files
28
+ - Logs refresh events to memory
29
+
30
+ **Slash Command Registration:**
31
+ - New `slash-commands.json` config for Claude Code integration
32
+ - Enables command discovery and validation
33
+ - Path: `{globalPath}/config/slash-commands.json`
34
+
35
+ **Token Budget Analysis:**
36
+ - Estimates context token usage during sync
37
+ - Warns if approaching 80% of budget (160k tokens)
38
+ - Auto-summarizes large agents if needed
39
+
40
+ **Client Migration on Update:**
41
+ - Setup now creates missing config files for existing projects
42
+ - `slash-commands.json` auto-created for existing installations
43
+ - Seamless upgrade experience for npm update users
44
+
45
+ **New TypeScript Modules:**
46
+ - `core/infrastructure/slash-command-registry.ts` - Command validation
47
+ - `core/agentic/token-estimator.ts` - Token budget estimation
48
+
49
+ **Template Context Optimization:**
50
+ - Reduced main templates from 3,197 to 2,214 lines (-31%)
51
+ - `sync.md`: 1,602 → 835 lines (-48%)
52
+ - `global/CLAUDE.md`: 429 → 363 lines (-15%)
53
+ - `task.md`: 397 → 336 lines (-15%)
54
+ - `ship.md`: 769 → 680 lines (-12%)
55
+ - New `templates/guides/` for on-demand documentation
56
+ - New `templates/shared/validation.md` for reusable validation
57
+
58
+ **Files Removed (Agentic Generation):**
59
+ - Deleted hardcoded domain agents from `templates/subagents/domain/`
60
+ - Deleted `.mcp.json` (now generated per-project)
61
+ - Deleted `CLAUDE.md` from root (moved to templates/global/)
62
+
63
+ ---
64
+
3
65
  ## [0.28.2] - 2026-01-10
4
66
 
5
67
  ### Feature: Agent Mentions and Major Cleanup
@@ -90,9 +90,19 @@ export { default as toolRegistry } from './tool-registry'
90
90
  export { default as templateLoader } from './template-loader'
91
91
 
92
92
  // ============ Utilities ============
93
- // Chain of thought, services
93
+ // Chain of thought, services, token estimation
94
94
  export { default as chainOfThought } from './chain-of-thought'
95
95
  export { default as services } from './services'
96
+ export {
97
+ default as tokenEstimator,
98
+ estimateTokens,
99
+ getTokenBudget,
100
+ estimateContext,
101
+ filterContext,
102
+ summarizeForTokens,
103
+ createContextSections,
104
+ formatEstimate,
105
+ } from './token-estimator'
96
106
 
97
107
  // ============ Types ============
98
108
  // All types re-exported from ../types (canonical source)
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Token Budget Estimator
3
+ * Estimates token usage and provides context filtering for large projects.
4
+ *
5
+ * This prevents context overflow by:
6
+ * - Estimating token count before sending to Claude
7
+ * - Prioritizing critical context over nice-to-have
8
+ * - Truncating or summarizing when needed
9
+ *
10
+ * @version 1.0.0
11
+ */
12
+
13
+ export interface TokenEstimate {
14
+ total: number
15
+ breakdown: {
16
+ projectContext: number
17
+ agentContext: number
18
+ skillContext: number
19
+ userPrompt: number
20
+ systemPrompt: number
21
+ }
22
+ withinBudget: boolean
23
+ recommendations: string[]
24
+ }
25
+
26
+ export interface ContextSection {
27
+ name: string
28
+ content: string
29
+ priority: 'critical' | 'high' | 'medium' | 'low'
30
+ tokens: number
31
+ }
32
+
33
+ /**
34
+ * Approximate tokens per character ratio
35
+ * Claude uses ~4 characters per token on average for English text
36
+ * Code tends to be slightly more efficient (~3.5 chars/token)
37
+ */
38
+ const CHARS_PER_TOKEN = 3.8
39
+
40
+ /**
41
+ * Default token budgets by model
42
+ */
43
+ const TOKEN_BUDGETS = {
44
+ 'claude-3-opus': 200000,
45
+ 'claude-3-sonnet': 200000,
46
+ 'claude-3-haiku': 200000,
47
+ 'claude-3.5-sonnet': 200000,
48
+ 'claude-opus-4': 200000,
49
+ default: 100000, // Conservative default
50
+ }
51
+
52
+ /**
53
+ * Estimate tokens from string content
54
+ */
55
+ export function estimateTokens(content: string): number {
56
+ if (!content) return 0
57
+ return Math.ceil(content.length / CHARS_PER_TOKEN)
58
+ }
59
+
60
+ /**
61
+ * Get token budget for a model
62
+ */
63
+ export function getTokenBudget(model: string = 'default'): number {
64
+ return TOKEN_BUDGETS[model as keyof typeof TOKEN_BUDGETS] || TOKEN_BUDGETS.default
65
+ }
66
+
67
+ /**
68
+ * Estimate total context tokens
69
+ */
70
+ export function estimateContext(sections: ContextSection[]): TokenEstimate {
71
+ const breakdown = {
72
+ projectContext: 0,
73
+ agentContext: 0,
74
+ skillContext: 0,
75
+ userPrompt: 0,
76
+ systemPrompt: 0,
77
+ }
78
+
79
+ let total = 0
80
+
81
+ for (const section of sections) {
82
+ const tokens = estimateTokens(section.content)
83
+ section.tokens = tokens
84
+ total += tokens
85
+
86
+ // Categorize for breakdown
87
+ if (section.name.includes('agent')) {
88
+ breakdown.agentContext += tokens
89
+ } else if (section.name.includes('skill')) {
90
+ breakdown.skillContext += tokens
91
+ } else if (section.name.includes('user') || section.name.includes('prompt')) {
92
+ breakdown.userPrompt += tokens
93
+ } else if (section.name.includes('system')) {
94
+ breakdown.systemPrompt += tokens
95
+ } else {
96
+ breakdown.projectContext += tokens
97
+ }
98
+ }
99
+
100
+ const budget = getTokenBudget()
101
+ const withinBudget = total < budget * 0.8 // Leave 20% buffer for response
102
+
103
+ const recommendations: string[] = []
104
+
105
+ if (!withinBudget) {
106
+ recommendations.push(`Context exceeds safe limit (${total} tokens vs ${Math.floor(budget * 0.8)} budget)`)
107
+
108
+ // Find sections that can be reduced
109
+ const lowPriority = sections.filter(s => s.priority === 'low')
110
+ if (lowPriority.length > 0) {
111
+ const lowTokens = lowPriority.reduce((sum, s) => sum + s.tokens, 0)
112
+ recommendations.push(`Remove low-priority sections to save ~${lowTokens} tokens`)
113
+ }
114
+
115
+ const mediumPriority = sections.filter(s => s.priority === 'medium')
116
+ if (mediumPriority.length > 0) {
117
+ const medTokens = mediumPriority.reduce((sum, s) => sum + s.tokens, 0)
118
+ recommendations.push(`Consider summarizing medium-priority sections (~${medTokens} tokens)`)
119
+ }
120
+ }
121
+
122
+ return {
123
+ total,
124
+ breakdown,
125
+ withinBudget,
126
+ recommendations,
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Filter context to fit within budget
132
+ */
133
+ export function filterContext(
134
+ sections: ContextSection[],
135
+ maxTokens?: number
136
+ ): { filtered: ContextSection[]; removed: string[]; totalTokens: number } {
137
+ const budget = maxTokens || getTokenBudget() * 0.8
138
+
139
+ // Sort by priority (critical first)
140
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }
141
+ const sorted = [...sections].sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority])
142
+
143
+ const filtered: ContextSection[] = []
144
+ const removed: string[] = []
145
+ let totalTokens = 0
146
+
147
+ for (const section of sorted) {
148
+ const sectionTokens = estimateTokens(section.content)
149
+
150
+ if (totalTokens + sectionTokens <= budget) {
151
+ filtered.push({ ...section, tokens: sectionTokens })
152
+ totalTokens += sectionTokens
153
+ } else if (section.priority === 'critical') {
154
+ // Critical sections are always included, even if over budget
155
+ filtered.push({ ...section, tokens: sectionTokens })
156
+ totalTokens += sectionTokens
157
+ } else {
158
+ removed.push(section.name)
159
+ }
160
+ }
161
+
162
+ return { filtered, removed, totalTokens }
163
+ }
164
+
165
+ /**
166
+ * Summarize content to reduce tokens
167
+ */
168
+ export function summarizeForTokens(content: string, targetTokens: number): string {
169
+ const currentTokens = estimateTokens(content)
170
+
171
+ if (currentTokens <= targetTokens) {
172
+ return content
173
+ }
174
+
175
+ // Calculate target character count
176
+ const targetChars = targetTokens * CHARS_PER_TOKEN
177
+
178
+ // Split into lines and take priority lines
179
+ const lines = content.split('\n')
180
+
181
+ // Keep headers and first lines of sections
182
+ const priorityLines: string[] = []
183
+ let charCount = 0
184
+
185
+ for (const line of lines) {
186
+ const isHeader = line.startsWith('#') || line.startsWith('**')
187
+ const isImportant = line.includes('CRITICAL') || line.includes('IMPORTANT') || line.includes('TODO')
188
+
189
+ if (isHeader || isImportant) {
190
+ priorityLines.push(line)
191
+ charCount += line.length + 1
192
+ } else if (charCount < targetChars * 0.8) {
193
+ priorityLines.push(line)
194
+ charCount += line.length + 1
195
+ }
196
+
197
+ if (charCount >= targetChars) {
198
+ break
199
+ }
200
+ }
201
+
202
+ if (priorityLines.length < lines.length) {
203
+ priorityLines.push('')
204
+ priorityLines.push(`[... ${lines.length - priorityLines.length} lines truncated for context limit ...]`)
205
+ }
206
+
207
+ return priorityLines.join('\n')
208
+ }
209
+
210
+ /**
211
+ * Create context sections from project state
212
+ */
213
+ export function createContextSections(
214
+ projectContext: string,
215
+ agentContext: string,
216
+ skillContext: string,
217
+ userPrompt: string
218
+ ): ContextSection[] {
219
+ return [
220
+ { name: 'user-prompt', content: userPrompt, priority: 'critical', tokens: 0 },
221
+ { name: 'agent-context', content: agentContext, priority: 'high', tokens: 0 },
222
+ { name: 'project-context', content: projectContext, priority: 'medium', tokens: 0 },
223
+ { name: 'skill-context', content: skillContext, priority: 'medium', tokens: 0 },
224
+ ]
225
+ }
226
+
227
+ /**
228
+ * Format token estimate for display
229
+ */
230
+ export function formatEstimate(estimate: TokenEstimate): string {
231
+ const lines = [
232
+ '📊 Token Budget',
233
+ '',
234
+ `Total: ${estimate.total.toLocaleString()} tokens`,
235
+ '',
236
+ 'Breakdown:',
237
+ ` Project: ${estimate.breakdown.projectContext.toLocaleString()}`,
238
+ ` Agents: ${estimate.breakdown.agentContext.toLocaleString()}`,
239
+ ` Skills: ${estimate.breakdown.skillContext.toLocaleString()}`,
240
+ ` Prompt: ${estimate.breakdown.userPrompt.toLocaleString()}`,
241
+ '',
242
+ `Status: ${estimate.withinBudget ? '✅ Within budget' : '⚠️ Over budget'}`,
243
+ ]
244
+
245
+ if (estimate.recommendations.length > 0) {
246
+ lines.push('')
247
+ lines.push('Recommendations:')
248
+ for (const rec of estimate.recommendations) {
249
+ lines.push(` - ${rec}`)
250
+ }
251
+ }
252
+
253
+ return lines.join('\n')
254
+ }
255
+
256
+ export default {
257
+ estimateTokens,
258
+ getTokenBudget,
259
+ estimateContext,
260
+ filterContext,
261
+ summarizeForTokens,
262
+ createContextSections,
263
+ formatEstimate,
264
+ }
@@ -146,9 +146,34 @@ async function migrateProjectsCliVersion(): Promise<void> {
146
146
  .map(dirent => dirent.name)
147
147
 
148
148
  let migrated = 0
149
+ let configsCreated = 0
149
150
 
150
151
  for (const projectId of projectDirs) {
151
152
  const projectJsonPath = path.join(projectsDir, projectId, 'project.json')
153
+ const configDir = path.join(projectsDir, projectId, 'config')
154
+
155
+ // Ensure config directory exists for new config files
156
+ if (!fs.existsSync(configDir)) {
157
+ fs.mkdirSync(configDir, { recursive: true })
158
+ }
159
+
160
+ // Create slash-commands.json if missing (v0.28+ feature)
161
+ const slashCommandsPath = path.join(configDir, 'slash-commands.json')
162
+ if (!fs.existsSync(slashCommandsPath)) {
163
+ const slashCommandsConfig = {
164
+ version: '1.0.0',
165
+ generatedAt: new Date().toISOString(),
166
+ note: 'Run /p:sync to regenerate with full command list',
167
+ commands: {
168
+ 'p:task': { description: 'Start any task', category: 'workflow' },
169
+ 'p:done': { description: 'Complete subtask', category: 'workflow' },
170
+ 'p:ship': { description: 'Ship feature', category: 'shipping' },
171
+ 'p:sync': { description: 'Sync project', category: 'planning' },
172
+ }
173
+ }
174
+ fs.writeFileSync(slashCommandsPath, JSON.stringify(slashCommandsConfig, null, 2))
175
+ configsCreated++
176
+ }
152
177
 
153
178
  if (!fs.existsSync(projectJsonPath)) {
154
179
  continue
@@ -172,6 +197,9 @@ async function migrateProjectsCliVersion(): Promise<void> {
172
197
  if (migrated > 0) {
173
198
  console.log(` ${GREEN}✓${NC} Updated ${migrated} project(s) to v${VERSION}`)
174
199
  }
200
+ if (configsCreated > 0) {
201
+ console.log(` ${GREEN}✓${NC} Created ${configsCreated} new config file(s)`)
202
+ }
175
203
  } catch {
176
204
  // Silently fail - migration is optional
177
205
  }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Slash Command Registry
3
+ * Generates Claude Code native slash command configuration.
4
+ *
5
+ * This enables:
6
+ * - Native /p:* command autocomplete in Claude Code
7
+ * - Command validation before execution
8
+ * - Command discoverability in help systems
9
+ *
10
+ * @version 1.0.0
11
+ */
12
+
13
+ import fs from 'fs/promises'
14
+ import path from 'path'
15
+ import os from 'os'
16
+
17
+ export interface SlashCommand {
18
+ name: string
19
+ description: string
20
+ args?: string
21
+ category: 'workflow' | 'planning' | 'shipping' | 'analytics' | 'maintenance'
22
+ requiresProject: boolean
23
+ deprecated?: boolean
24
+ replacedBy?: string
25
+ }
26
+
27
+ export interface SlashCommandRegistry {
28
+ version: string
29
+ generatedAt: string
30
+ commands: Record<string, SlashCommand>
31
+ }
32
+
33
+ /**
34
+ * Core prjct commands with metadata
35
+ */
36
+ const PRJCT_COMMANDS: SlashCommand[] = [
37
+ // Workflow
38
+ { name: 'task', description: 'Start any task with intelligent classification', args: '<description>', category: 'workflow', requiresProject: true },
39
+ { name: 'done', description: 'Complete current subtask', category: 'workflow', requiresProject: true },
40
+ { name: 'pause', description: 'Pause current task', category: 'workflow', requiresProject: true },
41
+ { name: 'resume', description: 'Resume paused task', category: 'workflow', requiresProject: true },
42
+ { name: 'next', description: 'Show next tasks in queue', category: 'workflow', requiresProject: true },
43
+
44
+ // Planning
45
+ { name: 'init', description: 'Initialize prjct in current directory', args: '[description]', category: 'planning', requiresProject: false },
46
+ { name: 'sync', description: 'Deep sync - analyze project, generate agents', category: 'planning', requiresProject: true },
47
+ { name: 'idea', description: 'Capture an idea for later', args: '<idea>', category: 'planning', requiresProject: true },
48
+ { name: 'spec', description: 'Generate feature specification', args: '<feature>', category: 'planning', requiresProject: true },
49
+ { name: 'bug', description: 'Report a bug with auto-priority', args: '<description>', category: 'planning', requiresProject: true },
50
+
51
+ // Shipping
52
+ { name: 'ship', description: 'Ship feature with PR workflow', args: '[feature]', category: 'shipping', requiresProject: true },
53
+ { name: 'review', description: 'Run code review on changes', category: 'shipping', requiresProject: true },
54
+ { name: 'test', description: 'Run tests for current changes', category: 'shipping', requiresProject: true },
55
+ { name: 'verify', description: 'Verify deployment', category: 'shipping', requiresProject: true },
56
+
57
+ // Analytics
58
+ { name: 'dash', description: 'Show project dashboard', category: 'analytics', requiresProject: true },
59
+ { name: 'history', description: 'Show task history', category: 'analytics', requiresProject: true },
60
+ { name: 'analyze', description: 'Analyze codebase', category: 'analytics', requiresProject: true },
61
+
62
+ // Maintenance
63
+ { name: 'cleanup', description: 'Clean up project files', category: 'maintenance', requiresProject: true },
64
+ { name: 'undo', description: 'Undo last action', category: 'maintenance', requiresProject: true },
65
+ { name: 'redo', description: 'Redo undone action', category: 'maintenance', requiresProject: true },
66
+
67
+ // Deprecated
68
+ { name: 'now', description: 'Start task (deprecated)', args: '<task>', category: 'workflow', requiresProject: true, deprecated: true, replacedBy: 'task' },
69
+ { name: 'feature', description: 'Plan feature (deprecated)', args: '<feature>', category: 'planning', requiresProject: true, deprecated: true, replacedBy: 'task' },
70
+ ]
71
+
72
+ /**
73
+ * Generate slash command registry for Claude Code
74
+ */
75
+ export async function generateRegistry(): Promise<SlashCommandRegistry> {
76
+ const commands: Record<string, SlashCommand> = {}
77
+
78
+ for (const cmd of PRJCT_COMMANDS) {
79
+ commands[`p:${cmd.name}`] = cmd
80
+ }
81
+
82
+ return {
83
+ version: '1.0.0',
84
+ generatedAt: new Date().toISOString(),
85
+ commands,
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Write registry to project's global path
91
+ */
92
+ export async function writeRegistry(projectId: string): Promise<{ success: boolean; path?: string; error?: string }> {
93
+ try {
94
+ const globalPath = path.join(os.homedir(), '.prjct-cli', 'projects', projectId, 'config')
95
+ await fs.mkdir(globalPath, { recursive: true })
96
+
97
+ const registry = await generateRegistry()
98
+ const registryPath = path.join(globalPath, 'slash-commands.json')
99
+
100
+ await fs.writeFile(registryPath, JSON.stringify(registry, null, 2), 'utf-8')
101
+
102
+ return { success: true, path: registryPath }
103
+ } catch (error) {
104
+ return { success: false, error: (error as Error).message }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Get command by name
110
+ */
111
+ export function getCommand(name: string): SlashCommand | undefined {
112
+ return PRJCT_COMMANDS.find(cmd => cmd.name === name)
113
+ }
114
+
115
+ /**
116
+ * Get all commands by category
117
+ */
118
+ export function getCommandsByCategory(category: SlashCommand['category']): SlashCommand[] {
119
+ return PRJCT_COMMANDS.filter(cmd => cmd.category === category && !cmd.deprecated)
120
+ }
121
+
122
+ /**
123
+ * Validate command exists
124
+ */
125
+ export function validateCommand(name: string): { valid: boolean; command?: SlashCommand; error?: string } {
126
+ const cmd = getCommand(name)
127
+
128
+ if (!cmd) {
129
+ return { valid: false, error: `Unknown command: ${name}` }
130
+ }
131
+
132
+ if (cmd.deprecated) {
133
+ return {
134
+ valid: true,
135
+ command: cmd,
136
+ error: `Command '${name}' is deprecated. Use '${cmd.replacedBy}' instead.`,
137
+ }
138
+ }
139
+
140
+ return { valid: true, command: cmd }
141
+ }
142
+
143
+ /**
144
+ * Format commands for help display
145
+ */
146
+ export function formatHelpText(): string {
147
+ const categories = ['workflow', 'planning', 'shipping', 'analytics', 'maintenance'] as const
148
+ const lines: string[] = ['# prjct Commands', '']
149
+
150
+ for (const category of categories) {
151
+ const cmds = getCommandsByCategory(category)
152
+ if (cmds.length === 0) continue
153
+
154
+ lines.push(`## ${category.charAt(0).toUpperCase() + category.slice(1)}`)
155
+ lines.push('')
156
+
157
+ for (const cmd of cmds) {
158
+ const args = cmd.args ? ` ${cmd.args}` : ''
159
+ lines.push(`- \`/p:${cmd.name}${args}\` - ${cmd.description}`)
160
+ }
161
+
162
+ lines.push('')
163
+ }
164
+
165
+ return lines.join('\n')
166
+ }
167
+
168
+ export default {
169
+ generateRegistry,
170
+ writeRegistry,
171
+ getCommand,
172
+ getCommandsByCategory,
173
+ validateCommand,
174
+ formatHelpText,
175
+ PRJCT_COMMANDS,
176
+ }
@@ -3,10 +3,37 @@
3
3
  * Types for external service integrations
4
4
  */
5
5
 
6
+ /**
7
+ * MCP Server Configuration
8
+ */
9
+ export interface McpServerConfig {
10
+ name: string
11
+ description?: string
12
+ command: string
13
+ args: string[]
14
+ enabled: boolean
15
+ linkedAgents?: string[]
16
+ autoLoad?: boolean
17
+ setupAt?: string
18
+ }
19
+
20
+ /**
21
+ * MCP Servers Configuration for a project
22
+ */
23
+ export interface McpServersConfig {
24
+ projectId: string
25
+ version: string
26
+ servers: Record<string, McpServerConfig>
27
+ agentMcpMap: Record<string, string[]>
28
+ }
29
+
6
30
  /**
7
31
  * Integrations Config
8
32
  * Container for all external integrations
9
33
  */
10
34
  export interface IntegrationsConfig {
11
- // Future integrations can be added here
35
+ mcp?: {
36
+ enabled: boolean
37
+ configPath: string
38
+ }
12
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "0.28.2",
3
+ "version": "0.28.3",
4
4
  "description": "Built for Claude - Ship fast, track progress, stay focused. Developer momentum tool for indie hackers.",
5
5
  "main": "core/index.ts",
6
6
  "bin": {