prjct-cli 0.28.3 → 0.28.4

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,48 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.28.4] - 2026-01-11
4
+
5
+ ### Feature: Claude Code UX Enhancements
6
+
7
+ Rich terminal experience for Claude Code - 100% ToS compliant.
8
+
9
+ **Status Line System:**
10
+ - Custom status line script showing prjct task, git branch, lines changed
11
+ - Context usage bar with color-coded warnings (green/yellow/red)
12
+ - Model icons (🎭 Opus, 📝 Sonnet, 🍃 Haiku)
13
+ - Installed at `~/.prjct-cli/statusline/statusline.sh`
14
+
15
+ **Theme System:**
16
+ - Three built-in themes: default (prjct), gentleman, minimal
17
+ - JSON-based theme configuration
18
+ - ANSI 256 color support
19
+ - Located at `~/.prjct-cli/statusline/themes/`
20
+
21
+ **Output Styles:**
22
+ - `prjct.md` - Concise, actionable output (default)
23
+ - `verbose.md` - Detailed explanations for learning
24
+ - `ship-fast.md` - Minimal output for max velocity
25
+ - Located at `~/.prjct-cli/output-styles/`
26
+
27
+ **Hooks Integration:**
28
+ - `update-metrics.sh` - Tracks tool usage per session
29
+ - `session-summary.sh` - Shows stats when Claude stops
30
+ - Located at `~/.prjct-cli/hooks/`
31
+
32
+ **Visual Theme (tweakcc):**
33
+ - Full Claude Code color theme
34
+ - Spanish thinking verbs
35
+ - Located at `~/.prjct-cli/tweakcc-theme.json`
36
+
37
+ **New Templates:**
38
+ - `templates/commands/setup-statusline.md` - Interactive setup
39
+ - `templates/guides/claude-code-ux.md` - Full documentation
40
+ - Updated `templates/global/CLAUDE.md` with UX section
41
+
42
+ **Template Optimization:**
43
+ - Further reduced template sizes
44
+ - New shared patterns in `templates/shared/`
45
+
3
46
  ## [0.28.3] - 2026-01-11
4
47
 
5
48
  ### Feature: Claude Code Synergy Optimization
@@ -43,7 +43,6 @@ import type {
43
43
  MemoryDatabase,
44
44
  HistoryEntry,
45
45
  HistoryEventType,
46
- Decision,
47
46
  Workflow,
48
47
  Preference,
49
48
  Patterns,
@@ -269,8 +268,13 @@ export class HistoryStore {
269
268
  /**
270
269
  * Patterns - Tier 2
271
270
  * Persistent learned preferences and decisions.
271
+ * OPTIMIZED: Debounced saves to reduce I/O
272
272
  */
273
273
  export class PatternStore extends CachedStore<Patterns> {
274
+ private _saveTimeout: ReturnType<typeof setTimeout> | null = null
275
+ private _pendingSave: string | null = null
276
+ private static readonly SAVE_DEBOUNCE_MS = 500 // Batch saves within 500ms
277
+
274
278
  protected getFilename(): string {
275
279
  return 'patterns.json'
276
280
  }
@@ -285,6 +289,41 @@ export class PatternStore extends CachedStore<Patterns> {
285
289
  }
286
290
  }
287
291
 
292
+ /**
293
+ * OPTIMIZED: Debounced save - batches multiple updates
294
+ */
295
+ private async debouncedSave(projectId: string): Promise<void> {
296
+ this._pendingSave = projectId
297
+
298
+ if (this._saveTimeout) {
299
+ clearTimeout(this._saveTimeout)
300
+ }
301
+
302
+ return new Promise((resolve) => {
303
+ this._saveTimeout = setTimeout(async () => {
304
+ if (this._pendingSave) {
305
+ await this.save(this._pendingSave)
306
+ this._pendingSave = null
307
+ }
308
+ resolve()
309
+ }, PatternStore.SAVE_DEBOUNCE_MS)
310
+ })
311
+ }
312
+
313
+ /**
314
+ * Force flush any pending saves (call before process exit)
315
+ */
316
+ async flush(projectId: string): Promise<void> {
317
+ if (this._saveTimeout) {
318
+ clearTimeout(this._saveTimeout)
319
+ this._saveTimeout = null
320
+ }
321
+ if (this._pendingSave) {
322
+ await this.save(projectId)
323
+ this._pendingSave = null
324
+ }
325
+ }
326
+
288
327
  // Convenience alias for backward compatibility
289
328
  async loadPatterns(projectId: string): Promise<Patterns> {
290
329
  return this.load(projectId)
@@ -330,7 +369,8 @@ export class PatternStore extends CachedStore<Patterns> {
330
369
  }
331
370
  }
332
371
 
333
- await this.save(projectId)
372
+ // OPTIMIZED: Debounced save instead of immediate
373
+ await this.debouncedSave(projectId)
334
374
  }
335
375
 
336
376
  async getDecision(projectId: string, key: string): Promise<{ value: string; confidence: string } | null> {
@@ -364,13 +404,12 @@ export class PatternStore extends CachedStore<Patterns> {
364
404
  patterns.workflows[workflowName].lastSeen = now
365
405
  }
366
406
 
367
- await this.save(projectId)
407
+ await this.debouncedSave(projectId)
368
408
  }
369
409
 
370
410
  async getWorkflow(projectId: string, workflowName: string): Promise<Workflow | null> {
371
411
  const patterns = await this.load(projectId)
372
412
  const workflow = patterns.workflows[workflowName]
373
-
374
413
  if (!workflow || workflow.count < 3) return null
375
414
  return workflow
376
415
  }
@@ -378,7 +417,7 @@ export class PatternStore extends CachedStore<Patterns> {
378
417
  async setPreference(projectId: string, key: string, value: Preference['value']): Promise<void> {
379
418
  const patterns = await this.load(projectId)
380
419
  patterns.preferences[key] = { value, updatedAt: getTimestamp() }
381
- await this.save(projectId)
420
+ await this.debouncedSave(projectId)
382
421
  }
383
422
 
384
423
  async getPreference(projectId: string, key: string, defaultValue: unknown = null): Promise<unknown> {
@@ -13,7 +13,6 @@ import { outcomeAnalyzer } from '../outcomes'
13
13
  import type { TaskType } from '../types'
14
14
  import type {
15
15
  ContextDomain,
16
- SmartContextProjectState,
17
16
  FullContext,
18
17
  FilteredContext,
19
18
  StackInfo,
@@ -32,8 +31,22 @@ export type {
32
31
  FilterMetrics,
33
32
  } from '../types'
34
33
 
35
- // Local type alias for backward compatibility
36
- type ProjectState = SmartContextProjectState
34
+ /**
35
+ * PERFORMANCE: Pre-built keyword → domain map for O(1) lookup
36
+ * Instead of iterating 5 arrays of keywords, we do a single pass
37
+ */
38
+ const KEYWORD_DOMAIN_MAP: Map<string, ContextDomain> = new Map([
39
+ // Frontend
40
+ ...['ui', 'component', 'react', 'vue', 'angular', 'css', 'style', 'button', 'form', 'modal', 'layout', 'responsive', 'animation', 'dom', 'html', 'frontend', 'fe', 'client', 'browser', 'jsx', 'tsx'].map(k => [k, 'frontend' as ContextDomain] as const),
41
+ // Backend
42
+ ...['api', 'server', 'database', 'db', 'endpoint', 'route', 'handler', 'controller', 'service', 'repository', 'model', 'query', 'backend', 'be', 'rest', 'graphql', 'prisma', 'sql', 'redis', 'auth'].map(k => [k, 'backend' as ContextDomain] as const),
43
+ // DevOps
44
+ ...['deploy', 'docker', 'kubernetes', 'k8s', 'ci', 'cd', 'pipeline', 'terraform', 'ansible', 'aws', 'gcp', 'azure', 'nginx', 'devops', 'infrastructure', 'monitoring', 'logging'].map(k => [k, 'devops' as ContextDomain] as const),
45
+ // Docs
46
+ ...['document', 'docs', 'readme', 'changelog', 'comment', 'jsdoc', 'tutorial', 'guide', 'explain', 'describe', 'markdown'].map(k => [k, 'docs' as ContextDomain] as const),
47
+ // Testing
48
+ ...['test', 'spec', 'jest', 'mocha', 'cypress', 'playwright', 'pytest', 'unittest', 'e2e', 'unit', 'integration', 'coverage', 'mock', 'fixture'].map(k => [k, 'testing' as ContextDomain] as const),
49
+ ])
37
50
 
38
51
  /**
39
52
  * SmartContext - Intelligent context filtering.
@@ -41,63 +54,20 @@ type ProjectState = SmartContextProjectState
41
54
  class SmartContext {
42
55
  /**
43
56
  * Detect the domain of a task from its description.
57
+ * OPTIMIZED: Single-pass O(n) where n = words in description
44
58
  */
45
59
  detectDomain(taskDescription: string): DomainAnalysis {
46
- const lower = taskDescription.toLowerCase()
47
-
48
- // Frontend indicators
49
- const frontendKeywords = [
50
- 'ui', 'component', 'react', 'vue', 'angular', 'css', 'style',
51
- 'button', 'form', 'modal', 'layout', 'responsive', 'animation',
52
- 'dom', 'html', 'frontend', 'fe', 'client', 'browser', 'jsx', 'tsx'
53
- ]
54
-
55
- // Backend indicators
56
- const backendKeywords = [
57
- 'api', 'server', 'database', 'db', 'endpoint', 'route', 'handler',
58
- 'controller', 'service', 'repository', 'model', 'query', 'backend',
59
- 'be', 'rest', 'graphql', 'prisma', 'sql', 'redis', 'auth'
60
- ]
61
-
62
- // DevOps indicators
63
- const devopsKeywords = [
64
- 'deploy', 'docker', 'kubernetes', 'k8s', 'ci', 'cd', 'pipeline',
65
- 'terraform', 'ansible', 'aws', 'gcp', 'azure', 'config', 'nginx',
66
- 'devops', 'infrastructure', 'monitoring', 'logging', 'build'
67
- ]
68
-
69
- // Docs indicators
70
- const docsKeywords = [
71
- 'document', 'docs', 'readme', 'changelog', 'comment', 'jsdoc',
72
- 'tutorial', 'guide', 'explain', 'describe', 'markdown'
73
- ]
74
-
75
- // Testing indicators
76
- const testingKeywords = [
77
- 'test', 'spec',
78
- // JS/TS
79
- 'bun', 'bun test', 'jest', 'mocha', 'cypress', 'playwright',
80
- // Python
81
- 'pytest', 'unittest',
82
- // Go
83
- 'go test',
84
- // Rust
85
- 'cargo test',
86
- // .NET
87
- 'dotnet test',
88
- // Java
89
- 'mvn test', 'gradle test', 'gradlew test',
90
- 'e2e', 'unit', 'integration', 'coverage', 'mock', 'fixture'
91
- ]
92
-
93
- // Count matches
60
+ const words = taskDescription.toLowerCase().split(/\s+/)
94
61
  const scores: Record<ContextDomain, number> = {
95
- frontend: frontendKeywords.filter(k => lower.includes(k)).length,
96
- backend: backendKeywords.filter(k => lower.includes(k)).length,
97
- devops: devopsKeywords.filter(k => lower.includes(k)).length,
98
- docs: docsKeywords.filter(k => lower.includes(k)).length,
99
- testing: testingKeywords.filter(k => lower.includes(k)).length,
100
- general: 0,
62
+ frontend: 0, backend: 0, devops: 0, docs: 0, testing: 0, general: 0
63
+ }
64
+
65
+ // Single pass: check each word against the map
66
+ for (const word of words) {
67
+ const domain = KEYWORD_DOMAIN_MAP.get(word)
68
+ if (domain) {
69
+ scores[domain]++
70
+ }
101
71
  }
102
72
 
103
73
  // Find primary and secondary domains
@@ -139,14 +109,16 @@ class SmartContext {
139
109
  agent => relevantDomains.includes(agent.domain)
140
110
  )
141
111
 
142
- // Enrich with performance data
143
- for (const agent of filteredAgents) {
144
- const perf = await agentPerformanceTracker.getAgentPerformance(
145
- projectId,
146
- agent.name
147
- )
112
+ // OPTIMIZED: Enrich with performance data in parallel
113
+ const perfPromises = filteredAgents.map(agent =>
114
+ agentPerformanceTracker.getAgentPerformance(projectId, agent.name)
115
+ )
116
+ const perfResults = await Promise.all(perfPromises)
117
+
118
+ for (let i = 0; i < filteredAgents.length; i++) {
119
+ const perf = perfResults[i]
148
120
  if (perf) {
149
- agent.successRate = perf.successRate
121
+ filteredAgents[i].successRate = perf.successRate
150
122
  }
151
123
  }
152
124
 
@@ -115,20 +115,20 @@ class PathManager {
115
115
  /**
116
116
  * Ensure the project-specific global structure exists
117
117
  * Creates the layered directory structure for a project
118
+ * OPTIMIZED: Parallel directory creation
118
119
  */
119
120
  async ensureProjectStructure(projectId: string): Promise<string> {
120
121
  await this.ensureGlobalStructure()
121
122
 
122
123
  const projectPath = this.getGlobalProjectPath(projectId)
123
124
 
124
- const layers = ['core', 'progress', 'planning', 'analysis', 'memory', 'agents']
125
+ // OPTIMIZED: Create all directories in parallel
126
+ const dirs = [
127
+ 'core', 'progress', 'planning', 'analysis', 'memory', 'agents',
128
+ 'planning/tasks', 'sessions', 'storage', 'context', 'sync'
129
+ ].map(dir => path.join(projectPath, dir))
125
130
 
126
- for (const layer of layers) {
127
- await fileHelper.ensureDir(path.join(projectPath, layer))
128
- }
129
-
130
- await fileHelper.ensureDir(path.join(projectPath, 'planning', 'tasks'))
131
- await fileHelper.ensureDir(path.join(projectPath, 'sessions'))
131
+ await Promise.all(dirs.map(dir => fileHelper.ensureDir(dir)))
132
132
 
133
133
  return projectPath
134
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "0.28.3",
3
+ "version": "0.28.4",
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": {