prjct-cli 1.7.2 → 1.7.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,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.3] - 2026-02-07
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
- add Zod validation and token budgets for prompt injection (PRJ-282) (#142)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [1.7.3] - 2026-02-07
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
- **Validate auto-injected state in prompt builder (PRJ-282)**: Added `safeInject()` validation utility, token-aware truncation via `InjectionBudgetTracker`, and domain-based skill filtering to prevent oversized or irrelevant content in LLM prompts. Replaced hardcoded character limits with configurable token budgets.
|
|
14
|
+
|
|
15
|
+
### Implementation Details
|
|
16
|
+
- Created `core/agentic/injection-validator.ts` with `safeInject()`, `safeInjectString()`, `truncateToTokenBudget()`, `estimateTokens()`, `filterSkillsByDomains()`, and `InjectionBudgetTracker` class
|
|
17
|
+
- Wired validation into `prompt-builder.ts`: auto-context truncation, agent/skill token budgets, cumulative state budget tracking
|
|
18
|
+
- Skills filtered by detected task domains before injection to reduce token waste
|
|
19
|
+
- 33 new unit tests covering all validation, filtering, and truncation paths
|
|
20
|
+
|
|
21
|
+
### Test Plan
|
|
22
|
+
|
|
23
|
+
#### For QA
|
|
24
|
+
1. Run `bun test` — all 526 tests pass (33 new)
|
|
25
|
+
2. Verify `safeInject()` returns fallback on corrupt data
|
|
26
|
+
3. Verify `filterSkillsByDomains()` excludes irrelevant skills
|
|
27
|
+
4. Verify `InjectionBudgetTracker` enforces cumulative limits
|
|
28
|
+
|
|
29
|
+
#### For Users
|
|
30
|
+
- No user-facing changes — validation is automatic
|
|
31
|
+
- Breaking changes: None
|
|
32
|
+
|
|
3
33
|
## [1.7.2] - 2026-02-07
|
|
4
34
|
|
|
5
35
|
### Bug Fixes
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injection Validator Tests
|
|
3
|
+
* Tests for safeInject, truncation, skill filtering, and budget tracking.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from 'bun:test'
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_BUDGETS,
|
|
10
|
+
estimateTokens,
|
|
11
|
+
filterSkillsByDomains,
|
|
12
|
+
InjectionBudgetTracker,
|
|
13
|
+
safeInject,
|
|
14
|
+
safeInjectString,
|
|
15
|
+
truncateToTokenBudget,
|
|
16
|
+
} from '../../agentic/injection-validator'
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// safeInject
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
describe('safeInject', () => {
|
|
23
|
+
const schema = z.object({ name: z.string(), value: z.number() })
|
|
24
|
+
const fallback = { name: 'unknown', value: 0 }
|
|
25
|
+
|
|
26
|
+
it('returns validated data on valid input', () => {
|
|
27
|
+
const data = { name: 'test', value: 42 }
|
|
28
|
+
expect(safeInject(data, schema, fallback)).toEqual(data)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('returns fallback on invalid input', () => {
|
|
32
|
+
const data = { name: 123, value: 'bad' }
|
|
33
|
+
expect(safeInject(data, schema, fallback)).toEqual(fallback)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('returns fallback on null input', () => {
|
|
37
|
+
expect(safeInject(null, schema, fallback)).toEqual(fallback)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('returns fallback on undefined input', () => {
|
|
41
|
+
expect(safeInject(undefined, schema, fallback)).toEqual(fallback)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('strips extra fields via Zod', () => {
|
|
45
|
+
const data = { name: 'test', value: 42, extra: 'ignored' }
|
|
46
|
+
const result = safeInject(data, schema, fallback)
|
|
47
|
+
expect(result.name).toBe('test')
|
|
48
|
+
expect(result.value).toBe(42)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// safeInjectString
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
describe('safeInjectString', () => {
|
|
57
|
+
const schema = z.object({ count: z.number() })
|
|
58
|
+
const formatter = (d: { count: number }) => `Items: ${d.count}`
|
|
59
|
+
|
|
60
|
+
it('returns formatted string on valid input', () => {
|
|
61
|
+
expect(safeInjectString({ count: 5 }, schema, formatter, 'N/A')).toBe('Items: 5')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('returns fallback string on invalid input', () => {
|
|
65
|
+
expect(safeInjectString({ count: 'bad' }, schema, formatter, 'N/A')).toBe('N/A')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('returns fallback string on null', () => {
|
|
69
|
+
expect(safeInjectString(null, schema, formatter, 'no data')).toBe('no data')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// truncateToTokenBudget
|
|
75
|
+
// =============================================================================
|
|
76
|
+
|
|
77
|
+
describe('truncateToTokenBudget', () => {
|
|
78
|
+
it('returns text unchanged if within budget', () => {
|
|
79
|
+
const text = 'short text'
|
|
80
|
+
expect(truncateToTokenBudget(text, 100)).toBe(text)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('truncates text that exceeds budget', () => {
|
|
84
|
+
const text = 'a'.repeat(500) // 500 chars = ~125 tokens
|
|
85
|
+
const result = truncateToTokenBudget(text, 50) // 50 tokens = 200 chars
|
|
86
|
+
expect(result.length).toBeLessThan(500)
|
|
87
|
+
expect(result).toContain('truncated')
|
|
88
|
+
expect(result).toContain('~50 tokens')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('truncates to exact char limit', () => {
|
|
92
|
+
const text = 'a'.repeat(100)
|
|
93
|
+
const result = truncateToTokenBudget(text, 10) // 10 tokens = 40 chars
|
|
94
|
+
expect(result.startsWith('a'.repeat(40))).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('handles empty string', () => {
|
|
98
|
+
expect(truncateToTokenBudget('', 100)).toBe('')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('handles zero budget', () => {
|
|
102
|
+
const result = truncateToTokenBudget('some text', 0)
|
|
103
|
+
expect(result).toContain('truncated')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// estimateTokens
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
describe('estimateTokens', () => {
|
|
112
|
+
it('estimates tokens at ~4 chars per token', () => {
|
|
113
|
+
expect(estimateTokens('a'.repeat(100))).toBe(25)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('rounds up partial tokens', () => {
|
|
117
|
+
expect(estimateTokens('abc')).toBe(1) // 3/4 = 0.75, ceil = 1
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('handles empty string', () => {
|
|
121
|
+
expect(estimateTokens('')).toBe(0)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// =============================================================================
|
|
126
|
+
// filterSkillsByDomains
|
|
127
|
+
// =============================================================================
|
|
128
|
+
|
|
129
|
+
describe('filterSkillsByDomains', () => {
|
|
130
|
+
const skills = [
|
|
131
|
+
{ name: 'react-patterns', content: 'React component patterns and hooks' },
|
|
132
|
+
{ name: 'api-design', content: 'RESTful API design and endpoint patterns' },
|
|
133
|
+
{ name: 'jest-testing', content: 'Jest test patterns and assertions' },
|
|
134
|
+
{ name: 'docker-deploy', content: 'Docker and Kubernetes deployment' },
|
|
135
|
+
{ name: 'general-coding', content: 'General coding best practices' },
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
it('returns all skills when no domains detected', () => {
|
|
139
|
+
expect(filterSkillsByDomains(skills, [])).toEqual(skills)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('returns all skills when skills array is empty', () => {
|
|
143
|
+
expect(filterSkillsByDomains([], ['frontend'])).toEqual([])
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('filters to frontend-relevant skills', () => {
|
|
147
|
+
const result = filterSkillsByDomains(skills, ['frontend'])
|
|
148
|
+
expect(result.some((s) => s.name === 'react-patterns')).toBe(true)
|
|
149
|
+
expect(result.some((s) => s.name === 'docker-deploy')).toBe(false)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('filters to backend-relevant skills', () => {
|
|
153
|
+
const result = filterSkillsByDomains(skills, ['backend'])
|
|
154
|
+
expect(result.some((s) => s.name === 'api-design')).toBe(true)
|
|
155
|
+
expect(result.some((s) => s.name === 'react-patterns')).toBe(false)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('filters to testing-relevant skills', () => {
|
|
159
|
+
const result = filterSkillsByDomains(skills, ['testing'])
|
|
160
|
+
expect(result.some((s) => s.name === 'jest-testing')).toBe(true)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('supports multiple domains', () => {
|
|
164
|
+
const result = filterSkillsByDomains(skills, ['frontend', 'testing'])
|
|
165
|
+
expect(result.some((s) => s.name === 'react-patterns')).toBe(true)
|
|
166
|
+
expect(result.some((s) => s.name === 'jest-testing')).toBe(true)
|
|
167
|
+
expect(result.some((s) => s.name === 'docker-deploy')).toBe(false)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('matches domain name itself as keyword', () => {
|
|
171
|
+
const customSkills = [{ name: 'devops-helper', content: 'general devops tools' }]
|
|
172
|
+
const result = filterSkillsByDomains(customSkills, ['devops'])
|
|
173
|
+
expect(result).toHaveLength(1)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('is case insensitive', () => {
|
|
177
|
+
const result = filterSkillsByDomains(skills, ['Frontend'])
|
|
178
|
+
expect(result.some((s) => s.name === 'react-patterns')).toBe(true)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// =============================================================================
|
|
183
|
+
// InjectionBudgetTracker
|
|
184
|
+
// =============================================================================
|
|
185
|
+
|
|
186
|
+
describe('InjectionBudgetTracker', () => {
|
|
187
|
+
it('tracks cumulative token usage', () => {
|
|
188
|
+
const tracker = new InjectionBudgetTracker({ totalPrompt: 100 })
|
|
189
|
+
tracker.addSection('a'.repeat(200), 100) // 200 chars = 50 tokens
|
|
190
|
+
expect(tracker.totalUsed).toBe(50)
|
|
191
|
+
expect(tracker.remaining).toBe(50)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('truncates sections to per-section budget', () => {
|
|
195
|
+
const tracker = new InjectionBudgetTracker({ totalPrompt: 1000 })
|
|
196
|
+
const result = tracker.addSection('a'.repeat(500), 50) // budget: 50 tokens = 200 chars
|
|
197
|
+
expect(result.length).toBeLessThan(500)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('returns empty string when total budget exhausted', () => {
|
|
201
|
+
const tracker = new InjectionBudgetTracker({ totalPrompt: 10 })
|
|
202
|
+
tracker.addSection('a'.repeat(100), 50) // uses all 10 tokens of total budget
|
|
203
|
+
const result = tracker.addSection('more content', 50)
|
|
204
|
+
expect(result).toBe('')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('fits content to remaining total budget', () => {
|
|
208
|
+
const tracker = new InjectionBudgetTracker({ totalPrompt: 30 })
|
|
209
|
+
tracker.addSection('a'.repeat(80), 30) // 80 chars = 20 tokens
|
|
210
|
+
// Remaining: 10 tokens
|
|
211
|
+
const result = tracker.addSection('b'.repeat(200), 100) // wants 100 tokens, only 10 left
|
|
212
|
+
expect(result.length).toBeLessThan(200)
|
|
213
|
+
expect(tracker.remaining).toBe(0)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('uses default budgets when none provided', () => {
|
|
217
|
+
const tracker = new InjectionBudgetTracker()
|
|
218
|
+
expect(tracker.config.totalPrompt).toBe(DEFAULT_BUDGETS.totalPrompt)
|
|
219
|
+
expect(tracker.config.autoContext).toBe(DEFAULT_BUDGETS.autoContext)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('allows partial budget overrides', () => {
|
|
223
|
+
const tracker = new InjectionBudgetTracker({ totalPrompt: 5000 })
|
|
224
|
+
expect(tracker.config.totalPrompt).toBe(5000)
|
|
225
|
+
expect(tracker.config.autoContext).toBe(DEFAULT_BUDGETS.autoContext) // unchanged
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('remaining never goes negative', () => {
|
|
229
|
+
const tracker = new InjectionBudgetTracker({ totalPrompt: 5 })
|
|
230
|
+
tracker.addSection('a'.repeat(1000), 500)
|
|
231
|
+
expect(tracker.remaining).toBe(0)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// =============================================================================
|
|
236
|
+
// DEFAULT_BUDGETS
|
|
237
|
+
// =============================================================================
|
|
238
|
+
|
|
239
|
+
describe('DEFAULT_BUDGETS', () => {
|
|
240
|
+
it('has all required fields', () => {
|
|
241
|
+
expect(DEFAULT_BUDGETS.autoContext).toBeGreaterThan(0)
|
|
242
|
+
expect(DEFAULT_BUDGETS.agentContent).toBeGreaterThan(0)
|
|
243
|
+
expect(DEFAULT_BUDGETS.skillContent).toBeGreaterThan(0)
|
|
244
|
+
expect(DEFAULT_BUDGETS.stateData).toBeGreaterThan(0)
|
|
245
|
+
expect(DEFAULT_BUDGETS.memories).toBeGreaterThan(0)
|
|
246
|
+
expect(DEFAULT_BUDGETS.totalPrompt).toBeGreaterThan(0)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('totalPrompt is larger than individual budgets', () => {
|
|
250
|
+
expect(DEFAULT_BUDGETS.totalPrompt).toBeGreaterThan(DEFAULT_BUDGETS.autoContext)
|
|
251
|
+
expect(DEFAULT_BUDGETS.totalPrompt).toBeGreaterThan(DEFAULT_BUDGETS.agentContent)
|
|
252
|
+
expect(DEFAULT_BUDGETS.totalPrompt).toBeGreaterThan(DEFAULT_BUDGETS.skillContent)
|
|
253
|
+
expect(DEFAULT_BUDGETS.totalPrompt).toBeGreaterThan(DEFAULT_BUDGETS.stateData)
|
|
254
|
+
})
|
|
255
|
+
})
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injection Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates data before auto-injection into LLM prompts.
|
|
5
|
+
* Corrupted or oversized data gets safe fallbacks instead of broken context.
|
|
6
|
+
*
|
|
7
|
+
* @module agentic/injection-validator
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { z } from 'zod'
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Token Budget Configuration
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/** Configurable token budgets per injection section */
|
|
17
|
+
export interface InjectionBudgets {
|
|
18
|
+
/** Auto-injected context (task + queue + patterns) */
|
|
19
|
+
autoContext: number
|
|
20
|
+
/** Per-agent content in orchestrator */
|
|
21
|
+
agentContent: number
|
|
22
|
+
/** Per-skill content in orchestrator */
|
|
23
|
+
skillContent: number
|
|
24
|
+
/** State data section */
|
|
25
|
+
stateData: number
|
|
26
|
+
/** Memories section */
|
|
27
|
+
memories: number
|
|
28
|
+
/** Total prompt ceiling (all sections combined) */
|
|
29
|
+
totalPrompt: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Default budgets (in estimated tokens, ~4 chars per token) */
|
|
33
|
+
export const DEFAULT_BUDGETS: InjectionBudgets = {
|
|
34
|
+
autoContext: 500,
|
|
35
|
+
agentContent: 400,
|
|
36
|
+
skillContent: 500,
|
|
37
|
+
stateData: 1000,
|
|
38
|
+
memories: 600,
|
|
39
|
+
totalPrompt: 8000,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Approximate chars-per-token ratio
|
|
43
|
+
const CHARS_PER_TOKEN = 4
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Safe Injection
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate data against a Zod schema before injection.
|
|
51
|
+
* Returns validated data on success, or the fallback on failure.
|
|
52
|
+
*/
|
|
53
|
+
export function safeInject<T>(data: unknown, schema: z.ZodType<T>, fallback: T): T {
|
|
54
|
+
const result = schema.safeParse(data)
|
|
55
|
+
if (result.success) {
|
|
56
|
+
return result.data
|
|
57
|
+
}
|
|
58
|
+
return fallback
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate and stringify data for prompt injection.
|
|
63
|
+
* Returns formatted string on success, or fallback string on failure.
|
|
64
|
+
*/
|
|
65
|
+
export function safeInjectString<T>(
|
|
66
|
+
data: unknown,
|
|
67
|
+
schema: z.ZodType<T>,
|
|
68
|
+
formatter: (valid: T) => string,
|
|
69
|
+
fallbackString: string
|
|
70
|
+
): string {
|
|
71
|
+
const result = schema.safeParse(data)
|
|
72
|
+
if (result.success) {
|
|
73
|
+
return formatter(result.data)
|
|
74
|
+
}
|
|
75
|
+
return fallbackString
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// Token-Aware Truncation
|
|
80
|
+
// =============================================================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Truncate text to fit within a token budget.
|
|
84
|
+
* Uses char-based estimation (~4 chars/token).
|
|
85
|
+
*/
|
|
86
|
+
export function truncateToTokenBudget(text: string, maxTokens: number): string {
|
|
87
|
+
const maxChars = maxTokens * CHARS_PER_TOKEN
|
|
88
|
+
if (text.length <= maxChars) return text
|
|
89
|
+
return `${text.substring(0, maxChars)}\n... (truncated to ~${maxTokens} tokens)`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Estimate token count for a string.
|
|
94
|
+
*/
|
|
95
|
+
export function estimateTokens(text: string): number {
|
|
96
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// Skill Filtering
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
/** Domain keywords for matching skills to task domains */
|
|
104
|
+
const DOMAIN_KEYWORDS: Record<string, string[]> = {
|
|
105
|
+
frontend: ['react', 'vue', 'svelte', 'css', 'html', 'ui', 'component', 'frontend', 'web', 'dom'],
|
|
106
|
+
backend: ['api', 'server', 'backend', 'endpoint', 'route', 'middleware', 'database', 'sql'],
|
|
107
|
+
testing: ['test', 'spec', 'jest', 'vitest', 'cypress', 'playwright', 'coverage', 'assert'],
|
|
108
|
+
devops: ['docker', 'ci', 'cd', 'deploy', 'kubernetes', 'terraform', 'pipeline', 'github-actions'],
|
|
109
|
+
docs: ['documentation', 'readme', 'guide', 'tutorial', 'markdown'],
|
|
110
|
+
design: ['design', 'ux', 'ui', 'figma', 'wireframe', 'layout', 'accessibility'],
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Filter skills by relevance to detected task domains.
|
|
115
|
+
* Returns only skills whose content matches the task's domains.
|
|
116
|
+
*/
|
|
117
|
+
export function filterSkillsByDomains(
|
|
118
|
+
skills: { name: string; content: string }[],
|
|
119
|
+
detectedDomains: string[]
|
|
120
|
+
): { name: string; content: string }[] {
|
|
121
|
+
if (detectedDomains.length === 0 || skills.length === 0) return skills
|
|
122
|
+
|
|
123
|
+
// Build keyword set from detected domains
|
|
124
|
+
const relevantKeywords = new Set<string>()
|
|
125
|
+
for (const domain of detectedDomains) {
|
|
126
|
+
const keywords = DOMAIN_KEYWORDS[domain.toLowerCase()]
|
|
127
|
+
if (keywords) {
|
|
128
|
+
for (const kw of keywords) relevantKeywords.add(kw)
|
|
129
|
+
}
|
|
130
|
+
// Also add the domain name itself
|
|
131
|
+
relevantKeywords.add(domain.toLowerCase())
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return skills.filter((skill) => {
|
|
135
|
+
const text = `${skill.name} ${skill.content}`.toLowerCase()
|
|
136
|
+
// Keep skill if any relevant keyword appears in its name or content
|
|
137
|
+
for (const kw of relevantKeywords) {
|
|
138
|
+
if (text.includes(kw)) return true
|
|
139
|
+
}
|
|
140
|
+
return false
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// Section Budget Tracker
|
|
146
|
+
// =============================================================================
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Tracks cumulative token usage across injection sections.
|
|
150
|
+
* Allows checking remaining budget before adding more content.
|
|
151
|
+
*/
|
|
152
|
+
export class InjectionBudgetTracker {
|
|
153
|
+
private used = 0
|
|
154
|
+
private budgets: InjectionBudgets
|
|
155
|
+
|
|
156
|
+
constructor(budgets: Partial<InjectionBudgets> = {}) {
|
|
157
|
+
this.budgets = { ...DEFAULT_BUDGETS, ...budgets }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Add content and return it (possibly truncated to fit budget) */
|
|
161
|
+
addSection(content: string, sectionBudget: number): string {
|
|
162
|
+
const truncated = truncateToTokenBudget(content, sectionBudget)
|
|
163
|
+
const tokens = estimateTokens(truncated)
|
|
164
|
+
|
|
165
|
+
// Check total budget
|
|
166
|
+
if (this.used + tokens > this.budgets.totalPrompt) {
|
|
167
|
+
const remaining = this.budgets.totalPrompt - this.used
|
|
168
|
+
if (remaining <= 0) return ''
|
|
169
|
+
const fitted = truncateToTokenBudget(truncated, remaining)
|
|
170
|
+
this.used += estimateTokens(fitted)
|
|
171
|
+
return fitted
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.used += tokens
|
|
175
|
+
return truncated
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Get remaining token budget */
|
|
179
|
+
get remaining(): number {
|
|
180
|
+
return Math.max(0, this.budgets.totalPrompt - this.used)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Get total tokens used */
|
|
184
|
+
get totalUsed(): number {
|
|
185
|
+
return this.used
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Get the budgets config */
|
|
189
|
+
get config(): InjectionBudgets {
|
|
190
|
+
return this.budgets
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -28,6 +28,12 @@ import type {
|
|
|
28
28
|
import { getErrorMessage, isNotFoundError } from '../types/fs'
|
|
29
29
|
import { fileExists } from '../utils/fs-helpers'
|
|
30
30
|
import { PACKAGE_ROOT } from '../utils/version'
|
|
31
|
+
import {
|
|
32
|
+
DEFAULT_BUDGETS,
|
|
33
|
+
filterSkillsByDomains,
|
|
34
|
+
InjectionBudgetTracker,
|
|
35
|
+
truncateToTokenBudget,
|
|
36
|
+
} from './injection-validator'
|
|
31
37
|
|
|
32
38
|
// Re-export types for convenience
|
|
33
39
|
export type {
|
|
@@ -288,7 +294,8 @@ class PromptBuilder {
|
|
|
288
294
|
parts.push('---')
|
|
289
295
|
parts.push('')
|
|
290
296
|
|
|
291
|
-
|
|
297
|
+
const result = parts.join('\n')
|
|
298
|
+
return truncateToTokenBudget(result, DEFAULT_BUDGETS.autoContext)
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
/**
|
|
@@ -464,25 +471,29 @@ class PromptBuilder {
|
|
|
464
471
|
if (agent.skills.length > 0) {
|
|
465
472
|
parts.push(`Skills: ${agent.skills.join(', ')}\n`)
|
|
466
473
|
}
|
|
467
|
-
//
|
|
468
|
-
const truncatedContent =
|
|
469
|
-
agent.content
|
|
470
|
-
|
|
471
|
-
|
|
474
|
+
// Truncate agent content to token budget
|
|
475
|
+
const truncatedContent = truncateToTokenBudget(
|
|
476
|
+
agent.content,
|
|
477
|
+
DEFAULT_BUDGETS.agentContent
|
|
478
|
+
)
|
|
472
479
|
parts.push(`\`\`\`markdown\n${truncatedContent}\n\`\`\`\n\n`)
|
|
473
480
|
}
|
|
474
481
|
}
|
|
475
482
|
|
|
476
|
-
//
|
|
477
|
-
|
|
483
|
+
// Filter skills by detected domains, then inject (truncated)
|
|
484
|
+
const relevantSkills = filterSkillsByDomains(
|
|
485
|
+
orchestratorContext.skills,
|
|
486
|
+
orchestratorContext.detectedDomains
|
|
487
|
+
)
|
|
488
|
+
if (relevantSkills.length > 0) {
|
|
478
489
|
parts.push('### LOADED SKILLS (From Agent Frontmatter)\n\n')
|
|
479
|
-
for (const skill of
|
|
490
|
+
for (const skill of relevantSkills) {
|
|
480
491
|
parts.push(`#### Skill: ${skill.name}\n`)
|
|
481
|
-
//
|
|
482
|
-
const truncatedContent =
|
|
483
|
-
skill.content
|
|
484
|
-
|
|
485
|
-
|
|
492
|
+
// Truncate skill content to token budget
|
|
493
|
+
const truncatedContent = truncateToTokenBudget(
|
|
494
|
+
skill.content,
|
|
495
|
+
DEFAULT_BUDGETS.skillContent
|
|
496
|
+
)
|
|
486
497
|
parts.push(`\`\`\`markdown\n${truncatedContent}\n\`\`\`\n\n`)
|
|
487
498
|
}
|
|
488
499
|
}
|
|
@@ -721,28 +732,21 @@ class PromptBuilder {
|
|
|
721
732
|
}
|
|
722
733
|
|
|
723
734
|
/**
|
|
724
|
-
* Filter state data to include only relevant portions for the prompt
|
|
735
|
+
* Filter state data to include only relevant portions for the prompt.
|
|
736
|
+
* Uses InjectionBudgetTracker to enforce cumulative token limits.
|
|
725
737
|
*/
|
|
726
738
|
filterRelevantState(state: State): string | null {
|
|
727
739
|
if (!state || Object.keys(state).length === 0) return null
|
|
728
740
|
|
|
741
|
+
const tracker = new InjectionBudgetTracker({ totalPrompt: DEFAULT_BUDGETS.stateData })
|
|
742
|
+
const criticalFiles = ['now', 'next', 'context', 'analysis', 'codePatterns']
|
|
729
743
|
const relevant: string[] = []
|
|
744
|
+
|
|
730
745
|
for (const [key, content] of Object.entries(state)) {
|
|
731
746
|
if (content && (content as string).trim()) {
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
(content as string).length > 2000
|
|
736
|
-
? `${(content as string).substring(0, 2000)}\n... (truncated)`
|
|
737
|
-
: content
|
|
738
|
-
relevant.push(`### ${key}\n${display}`)
|
|
739
|
-
} else if ((content as string).length < 1000) {
|
|
740
|
-
relevant.push(`### ${key}\n${content}`)
|
|
741
|
-
} else {
|
|
742
|
-
relevant.push(
|
|
743
|
-
`### ${key}\n${(content as string).substring(0, 500)}... (truncated, use Read tool for full content)`
|
|
744
|
-
)
|
|
745
|
-
}
|
|
747
|
+
const sectionBudget = criticalFiles.includes(key) ? 500 : 250
|
|
748
|
+
const section = tracker.addSection(`### ${key}\n${content}`, sectionBudget)
|
|
749
|
+
if (section) relevant.push(section)
|
|
746
750
|
}
|
|
747
751
|
}
|
|
748
752
|
|
|
@@ -793,7 +797,8 @@ class PromptBuilder {
|
|
|
793
797
|
parts.push(`\nAvoid:\n${antiPatterns}`)
|
|
794
798
|
}
|
|
795
799
|
|
|
796
|
-
const
|
|
800
|
+
const joined = parts.join('\n')
|
|
801
|
+
const result = truncateToTokenBudget(joined, 200) // ~800 chars
|
|
797
802
|
return result || null
|
|
798
803
|
}
|
|
799
804
|
|
package/dist/bin/prjct.mjs
CHANGED
|
@@ -14219,6 +14219,97 @@ var init_outcomes2 = __esm({
|
|
|
14219
14219
|
}
|
|
14220
14220
|
});
|
|
14221
14221
|
|
|
14222
|
+
// core/agentic/injection-validator.ts
|
|
14223
|
+
function truncateToTokenBudget(text, maxTokens) {
|
|
14224
|
+
const maxChars = maxTokens * CHARS_PER_TOKEN2;
|
|
14225
|
+
if (text.length <= maxChars) return text;
|
|
14226
|
+
return `${text.substring(0, maxChars)}
|
|
14227
|
+
... (truncated to ~${maxTokens} tokens)`;
|
|
14228
|
+
}
|
|
14229
|
+
function estimateTokens(text) {
|
|
14230
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN2);
|
|
14231
|
+
}
|
|
14232
|
+
function filterSkillsByDomains(skills, detectedDomains) {
|
|
14233
|
+
if (detectedDomains.length === 0 || skills.length === 0) return skills;
|
|
14234
|
+
const relevantKeywords = /* @__PURE__ */ new Set();
|
|
14235
|
+
for (const domain of detectedDomains) {
|
|
14236
|
+
const keywords = DOMAIN_KEYWORDS3[domain.toLowerCase()];
|
|
14237
|
+
if (keywords) {
|
|
14238
|
+
for (const kw of keywords) relevantKeywords.add(kw);
|
|
14239
|
+
}
|
|
14240
|
+
relevantKeywords.add(domain.toLowerCase());
|
|
14241
|
+
}
|
|
14242
|
+
return skills.filter((skill) => {
|
|
14243
|
+
const text = `${skill.name} ${skill.content}`.toLowerCase();
|
|
14244
|
+
for (const kw of relevantKeywords) {
|
|
14245
|
+
if (text.includes(kw)) return true;
|
|
14246
|
+
}
|
|
14247
|
+
return false;
|
|
14248
|
+
});
|
|
14249
|
+
}
|
|
14250
|
+
var DEFAULT_BUDGETS, CHARS_PER_TOKEN2, DOMAIN_KEYWORDS3, InjectionBudgetTracker;
|
|
14251
|
+
var init_injection_validator = __esm({
|
|
14252
|
+
"core/agentic/injection-validator.ts"() {
|
|
14253
|
+
"use strict";
|
|
14254
|
+
DEFAULT_BUDGETS = {
|
|
14255
|
+
autoContext: 500,
|
|
14256
|
+
agentContent: 400,
|
|
14257
|
+
skillContent: 500,
|
|
14258
|
+
stateData: 1e3,
|
|
14259
|
+
memories: 600,
|
|
14260
|
+
totalPrompt: 8e3
|
|
14261
|
+
};
|
|
14262
|
+
CHARS_PER_TOKEN2 = 4;
|
|
14263
|
+
__name(truncateToTokenBudget, "truncateToTokenBudget");
|
|
14264
|
+
__name(estimateTokens, "estimateTokens");
|
|
14265
|
+
DOMAIN_KEYWORDS3 = {
|
|
14266
|
+
frontend: ["react", "vue", "svelte", "css", "html", "ui", "component", "frontend", "web", "dom"],
|
|
14267
|
+
backend: ["api", "server", "backend", "endpoint", "route", "middleware", "database", "sql"],
|
|
14268
|
+
testing: ["test", "spec", "jest", "vitest", "cypress", "playwright", "coverage", "assert"],
|
|
14269
|
+
devops: ["docker", "ci", "cd", "deploy", "kubernetes", "terraform", "pipeline", "github-actions"],
|
|
14270
|
+
docs: ["documentation", "readme", "guide", "tutorial", "markdown"],
|
|
14271
|
+
design: ["design", "ux", "ui", "figma", "wireframe", "layout", "accessibility"]
|
|
14272
|
+
};
|
|
14273
|
+
__name(filterSkillsByDomains, "filterSkillsByDomains");
|
|
14274
|
+
InjectionBudgetTracker = class {
|
|
14275
|
+
static {
|
|
14276
|
+
__name(this, "InjectionBudgetTracker");
|
|
14277
|
+
}
|
|
14278
|
+
used = 0;
|
|
14279
|
+
budgets;
|
|
14280
|
+
constructor(budgets = {}) {
|
|
14281
|
+
this.budgets = { ...DEFAULT_BUDGETS, ...budgets };
|
|
14282
|
+
}
|
|
14283
|
+
/** Add content and return it (possibly truncated to fit budget) */
|
|
14284
|
+
addSection(content, sectionBudget) {
|
|
14285
|
+
const truncated = truncateToTokenBudget(content, sectionBudget);
|
|
14286
|
+
const tokens = estimateTokens(truncated);
|
|
14287
|
+
if (this.used + tokens > this.budgets.totalPrompt) {
|
|
14288
|
+
const remaining = this.budgets.totalPrompt - this.used;
|
|
14289
|
+
if (remaining <= 0) return "";
|
|
14290
|
+
const fitted = truncateToTokenBudget(truncated, remaining);
|
|
14291
|
+
this.used += estimateTokens(fitted);
|
|
14292
|
+
return fitted;
|
|
14293
|
+
}
|
|
14294
|
+
this.used += tokens;
|
|
14295
|
+
return truncated;
|
|
14296
|
+
}
|
|
14297
|
+
/** Get remaining token budget */
|
|
14298
|
+
get remaining() {
|
|
14299
|
+
return Math.max(0, this.budgets.totalPrompt - this.used);
|
|
14300
|
+
}
|
|
14301
|
+
/** Get total tokens used */
|
|
14302
|
+
get totalUsed() {
|
|
14303
|
+
return this.used;
|
|
14304
|
+
}
|
|
14305
|
+
/** Get the budgets config */
|
|
14306
|
+
get config() {
|
|
14307
|
+
return this.budgets;
|
|
14308
|
+
}
|
|
14309
|
+
};
|
|
14310
|
+
}
|
|
14311
|
+
});
|
|
14312
|
+
|
|
14222
14313
|
// core/agentic/prompt-builder.ts
|
|
14223
14314
|
import fs29 from "node:fs/promises";
|
|
14224
14315
|
import path28 from "node:path";
|
|
@@ -14231,6 +14322,7 @@ var init_prompt_builder = __esm({
|
|
|
14231
14322
|
init_fs();
|
|
14232
14323
|
init_fs_helpers();
|
|
14233
14324
|
init_version();
|
|
14325
|
+
init_injection_validator();
|
|
14234
14326
|
PromptBuilder = class {
|
|
14235
14327
|
static {
|
|
14236
14328
|
__name(this, "PromptBuilder");
|
|
@@ -14426,7 +14518,8 @@ var init_prompt_builder = __esm({
|
|
|
14426
14518
|
}
|
|
14427
14519
|
parts.push("---");
|
|
14428
14520
|
parts.push("");
|
|
14429
|
-
|
|
14521
|
+
const result = parts.join("\n");
|
|
14522
|
+
return truncateToTokenBudget(result, DEFAULT_BUDGETS.autoContext);
|
|
14430
14523
|
}
|
|
14431
14524
|
/**
|
|
14432
14525
|
* Calculate elapsed time from ISO timestamp.
|
|
@@ -14560,8 +14653,10 @@ Apply specialized expertise. Read agent file for details if needed.
|
|
|
14560
14653
|
parts.push(`Skills: ${agent2.skills.join(", ")}
|
|
14561
14654
|
`);
|
|
14562
14655
|
}
|
|
14563
|
-
const truncatedContent =
|
|
14564
|
-
|
|
14656
|
+
const truncatedContent = truncateToTokenBudget(
|
|
14657
|
+
agent2.content,
|
|
14658
|
+
DEFAULT_BUDGETS.agentContent
|
|
14659
|
+
);
|
|
14565
14660
|
parts.push(`\`\`\`markdown
|
|
14566
14661
|
${truncatedContent}
|
|
14567
14662
|
\`\`\`
|
|
@@ -14569,13 +14664,19 @@ ${truncatedContent}
|
|
|
14569
14664
|
`);
|
|
14570
14665
|
}
|
|
14571
14666
|
}
|
|
14572
|
-
|
|
14667
|
+
const relevantSkills = filterSkillsByDomains(
|
|
14668
|
+
orchestratorContext.skills,
|
|
14669
|
+
orchestratorContext.detectedDomains
|
|
14670
|
+
);
|
|
14671
|
+
if (relevantSkills.length > 0) {
|
|
14573
14672
|
parts.push("### LOADED SKILLS (From Agent Frontmatter)\n\n");
|
|
14574
|
-
for (const skill of
|
|
14673
|
+
for (const skill of relevantSkills) {
|
|
14575
14674
|
parts.push(`#### Skill: ${skill.name}
|
|
14576
14675
|
`);
|
|
14577
|
-
const truncatedContent =
|
|
14578
|
-
|
|
14676
|
+
const truncatedContent = truncateToTokenBudget(
|
|
14677
|
+
skill.content,
|
|
14678
|
+
DEFAULT_BUDGETS.skillContent
|
|
14679
|
+
);
|
|
14579
14680
|
parts.push(`\`\`\`markdown
|
|
14580
14681
|
${truncatedContent}
|
|
14581
14682
|
\`\`\`
|
|
@@ -14803,28 +14904,20 @@ Show changes, list affected files, ask for confirmation.
|
|
|
14803
14904
|
return parts.join("");
|
|
14804
14905
|
}
|
|
14805
14906
|
/**
|
|
14806
|
-
* Filter state data to include only relevant portions for the prompt
|
|
14907
|
+
* Filter state data to include only relevant portions for the prompt.
|
|
14908
|
+
* Uses InjectionBudgetTracker to enforce cumulative token limits.
|
|
14807
14909
|
*/
|
|
14808
14910
|
filterRelevantState(state) {
|
|
14809
14911
|
if (!state || Object.keys(state).length === 0) return null;
|
|
14912
|
+
const tracker = new InjectionBudgetTracker({ totalPrompt: DEFAULT_BUDGETS.stateData });
|
|
14913
|
+
const criticalFiles = ["now", "next", "context", "analysis", "codePatterns"];
|
|
14810
14914
|
const relevant = [];
|
|
14811
14915
|
for (const [key, content] of Object.entries(state)) {
|
|
14812
14916
|
if (content && content.trim()) {
|
|
14813
|
-
const
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
relevant.push(`### ${key}
|
|
14818
|
-
${display}`);
|
|
14819
|
-
} else if (content.length < 1e3) {
|
|
14820
|
-
relevant.push(`### ${key}
|
|
14821
|
-
${content}`);
|
|
14822
|
-
} else {
|
|
14823
|
-
relevant.push(
|
|
14824
|
-
`### ${key}
|
|
14825
|
-
${content.substring(0, 500)}... (truncated, use Read tool for full content)`
|
|
14826
|
-
);
|
|
14827
|
-
}
|
|
14917
|
+
const sectionBudget = criticalFiles.includes(key) ? 500 : 250;
|
|
14918
|
+
const section = tracker.addSection(`### ${key}
|
|
14919
|
+
${content}`, sectionBudget);
|
|
14920
|
+
if (section) relevant.push(section);
|
|
14828
14921
|
}
|
|
14829
14922
|
}
|
|
14830
14923
|
return relevant.length > 0 ? relevant.join("\n\n") : null;
|
|
@@ -14865,7 +14958,8 @@ ${content.substring(0, 500)}... (truncated, use Read tool for full content)`
|
|
|
14865
14958
|
Avoid:
|
|
14866
14959
|
${antiPatterns}`);
|
|
14867
14960
|
}
|
|
14868
|
-
const
|
|
14961
|
+
const joined = parts.join("\n");
|
|
14962
|
+
const result = truncateToTokenBudget(joined, 200);
|
|
14869
14963
|
return result || null;
|
|
14870
14964
|
}
|
|
14871
14965
|
/**
|
|
@@ -16233,13 +16327,13 @@ var init_breakdown_service = __esm({
|
|
|
16233
16327
|
});
|
|
16234
16328
|
|
|
16235
16329
|
// core/services/context-selector.ts
|
|
16236
|
-
var DEFAULT_TOKEN_BUDGET,
|
|
16330
|
+
var DEFAULT_TOKEN_BUDGET, DOMAIN_KEYWORDS4, ContextSelector, contextSelector;
|
|
16237
16331
|
var init_context_selector = __esm({
|
|
16238
16332
|
"core/services/context-selector.ts"() {
|
|
16239
16333
|
"use strict";
|
|
16240
16334
|
init_index_storage();
|
|
16241
16335
|
DEFAULT_TOKEN_BUDGET = 8e4;
|
|
16242
|
-
|
|
16336
|
+
DOMAIN_KEYWORDS4 = {
|
|
16243
16337
|
payments: [
|
|
16244
16338
|
"payment",
|
|
16245
16339
|
"pay",
|
|
@@ -16422,7 +16516,7 @@ var init_context_selector = __esm({
|
|
|
16422
16516
|
detectTaskDomains(description, projectDomains) {
|
|
16423
16517
|
const normalizedDesc = description.toLowerCase();
|
|
16424
16518
|
const detectedDomains = /* @__PURE__ */ new Set();
|
|
16425
|
-
for (const [domain, keywords] of Object.entries(
|
|
16519
|
+
for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS4)) {
|
|
16426
16520
|
for (const keyword of keywords) {
|
|
16427
16521
|
if (normalizedDesc.includes(keyword)) {
|
|
16428
16522
|
detectedDomains.add(domain);
|
|
@@ -19043,8 +19137,8 @@ var init_analyzer2 = __esm({
|
|
|
19043
19137
|
|
|
19044
19138
|
// core/services/diff-generator.ts
|
|
19045
19139
|
import chalk11 from "chalk";
|
|
19046
|
-
function
|
|
19047
|
-
return Math.ceil(content.length /
|
|
19140
|
+
function estimateTokens2(content) {
|
|
19141
|
+
return Math.ceil(content.length / CHARS_PER_TOKEN3);
|
|
19048
19142
|
}
|
|
19049
19143
|
function parseMarkdownSections(content) {
|
|
19050
19144
|
const lines = content.split("\n");
|
|
@@ -19087,8 +19181,8 @@ function generateSyncDiff(oldContent, newContent) {
|
|
|
19087
19181
|
modified: [],
|
|
19088
19182
|
removed: [],
|
|
19089
19183
|
preserved: [],
|
|
19090
|
-
tokensBefore:
|
|
19091
|
-
tokensAfter:
|
|
19184
|
+
tokensBefore: estimateTokens2(oldContent),
|
|
19185
|
+
tokensAfter: estimateTokens2(newContent),
|
|
19092
19186
|
tokenDelta: 0
|
|
19093
19187
|
};
|
|
19094
19188
|
diff.tokenDelta = diff.tokensAfter - diff.tokensBefore;
|
|
@@ -19242,12 +19336,12 @@ function formatFullDiff(diff, options = {}) {
|
|
|
19242
19336
|
}
|
|
19243
19337
|
return lines.join("\n");
|
|
19244
19338
|
}
|
|
19245
|
-
var
|
|
19339
|
+
var CHARS_PER_TOKEN3;
|
|
19246
19340
|
var init_diff_generator = __esm({
|
|
19247
19341
|
"core/services/diff-generator.ts"() {
|
|
19248
19342
|
"use strict";
|
|
19249
|
-
|
|
19250
|
-
__name(
|
|
19343
|
+
CHARS_PER_TOKEN3 = 4;
|
|
19344
|
+
__name(estimateTokens2, "estimateTokens");
|
|
19251
19345
|
__name(parseMarkdownSections, "parseMarkdownSections");
|
|
19252
19346
|
__name(isPreservedSection, "isPreservedSection");
|
|
19253
19347
|
__name(generateSyncDiff, "generateSyncDiff");
|
|
@@ -23429,7 +23523,7 @@ You are the ${name} expert for this project. Apply best practices for the detect
|
|
|
23429
23523
|
* Token estimation: ~4 chars per token (industry standard)
|
|
23430
23524
|
*/
|
|
23431
23525
|
async recordSyncMetrics(stats, contextFiles, agents, duration) {
|
|
23432
|
-
const
|
|
23526
|
+
const CHARS_PER_TOKEN4 = 4;
|
|
23433
23527
|
let filteredChars = 0;
|
|
23434
23528
|
for (const file of contextFiles) {
|
|
23435
23529
|
try {
|
|
@@ -23452,7 +23546,7 @@ You are the ${name} expert for this project. Apply best practices for the detect
|
|
|
23452
23546
|
});
|
|
23453
23547
|
}
|
|
23454
23548
|
}
|
|
23455
|
-
const filteredSize = Math.floor(filteredChars /
|
|
23549
|
+
const filteredSize = Math.floor(filteredChars / CHARS_PER_TOKEN4);
|
|
23456
23550
|
const avgTokensPerFile = 500;
|
|
23457
23551
|
const originalSize = stats.fileCount * avgTokensPerFile;
|
|
23458
23552
|
const compressionRate = originalSize > 0 ? Math.max(0, (originalSize - filteredSize) / originalSize) : 0;
|
|
@@ -29016,7 +29110,7 @@ var require_package = __commonJS({
|
|
|
29016
29110
|
"package.json"(exports, module) {
|
|
29017
29111
|
module.exports = {
|
|
29018
29112
|
name: "prjct-cli",
|
|
29019
|
-
version: "1.7.
|
|
29113
|
+
version: "1.7.3",
|
|
29020
29114
|
description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
|
|
29021
29115
|
main: "core/index.ts",
|
|
29022
29116
|
bin: {
|