prjct-cli 1.7.3 → 1.7.5
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 +66 -0
- package/core/__tests__/agentic/cache-eviction.test.ts +294 -0
- package/core/agentic/context-builder.ts +31 -49
- package/core/agentic/memory-system.ts +62 -0
- package/core/integrations/linear/client.ts +3 -2
- package/core/session/session-log-manager.ts +11 -8
- package/dist/bin/prjct.mjs +75 -45
- package/package.json +2 -4
- package/scripts/build.js +0 -169
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.5] - 2026-02-07
|
|
4
|
+
|
|
5
|
+
### Refactoring
|
|
6
|
+
|
|
7
|
+
- remove unused deps and lazy-load @linear/sdk (PRJ-291) (#144)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [1.7.5] - 2026-02-07
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **Remove unused dependencies and lazy-load heavy optional ones (PRJ-291)**: Removed `lightningcss` (completely unused), moved `esbuild` to devDependencies (build-time only), lazy-loaded `@linear/sdk` via dynamic `import()` so it only loads when Linear commands are invoked.
|
|
14
|
+
|
|
15
|
+
### Implementation Details
|
|
16
|
+
- Removed `lightningcss` from dependencies (zero imports in codebase)
|
|
17
|
+
- Moved `esbuild` from dependencies to devDependencies (only used in `scripts/build.js`)
|
|
18
|
+
- Changed `import { LinearClient } from '@linear/sdk'` to `import type` + dynamic `await import('@linear/sdk')` in `core/integrations/linear/client.ts`
|
|
19
|
+
- Excluded test files from published package via `.npmignore`
|
|
20
|
+
- Removed `scripts/build.js` from `files` field (dist/ ships pre-built)
|
|
21
|
+
|
|
22
|
+
### Learnings
|
|
23
|
+
- `import type` + dynamic `await import()` pattern preserves full type safety while deferring module load to runtime. Type imports are erased at compile time with zero cost.
|
|
24
|
+
|
|
25
|
+
### Test Plan
|
|
26
|
+
|
|
27
|
+
#### For QA
|
|
28
|
+
1. Run `bun test` — 538 tests pass, no regressions
|
|
29
|
+
2. Run `bun run build` — compiles without errors
|
|
30
|
+
3. Run `bun run typecheck` — zero type errors
|
|
31
|
+
4. Run `prjct status` and `prjct linear list` — CLI works normally
|
|
32
|
+
|
|
33
|
+
#### For Users
|
|
34
|
+
**What changed:** Faster install (~75MB fewer dependencies), faster CLI startup (Linear SDK only loaded on demand).
|
|
35
|
+
**How to use:** No changes needed.
|
|
36
|
+
**Breaking changes:** None
|
|
37
|
+
|
|
38
|
+
## [1.7.4] - 2026-02-07
|
|
39
|
+
|
|
40
|
+
### Bug Fixes
|
|
41
|
+
|
|
42
|
+
- add eviction policies to all in-memory caches (PRJ-288) (#143)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## [1.7.4] - 2026-02-07
|
|
46
|
+
|
|
47
|
+
### Bug Fixes
|
|
48
|
+
- **Add eviction policies to all in-memory caches (PRJ-288)**: Replaced unbounded Maps with TTLCache in SessionLogManager and ContextBuilder, capped PatternStore decision contexts at 20 with FIFO eviction, and added 90-day archival for stale decisions to prevent unbounded memory growth.
|
|
49
|
+
|
|
50
|
+
### Implementation Details
|
|
51
|
+
- SessionLogManager: replaced 2 `Map` caches with `TTLCache` (maxSize: 50, TTL: 1hr)
|
|
52
|
+
- ContextBuilder: replaced `Map` + `_mtimes` + manual TTL with single `TTLCache<CachedFile>` (maxSize: 200, TTL: 5s), added project-switch detection
|
|
53
|
+
- PatternStore: added `afterLoad()` hook to truncate oversized contexts arrays, FIFO cap at 20 in `recordDecision()`, new `archiveStaleDecisions()` method for 90-day archival
|
|
54
|
+
- Exposed `archiveStaleDecisions()` via MemorySystem facade
|
|
55
|
+
|
|
56
|
+
### Test Plan
|
|
57
|
+
|
|
58
|
+
#### For QA
|
|
59
|
+
1. Run `bun test core/__tests__/agentic/cache-eviction.test.ts` — 12 new tests pass
|
|
60
|
+
2. Run `bun test` — full suite (538 tests) passes with no regressions
|
|
61
|
+
3. Run `bun run build` — compiles without errors
|
|
62
|
+
4. Verify `prjct sync` and `prjct status` work normally
|
|
63
|
+
|
|
64
|
+
#### For Users
|
|
65
|
+
**What changed:** Internal optimization — in-memory caches now bounded to prevent memory growth during long sessions.
|
|
66
|
+
**How to use:** No user-facing changes.
|
|
67
|
+
**Breaking changes:** None
|
|
68
|
+
|
|
3
69
|
## [1.7.3] - 2026-02-07
|
|
4
70
|
|
|
5
71
|
### Bug Fixes
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Eviction Policy Tests (PRJ-288)
|
|
3
|
+
* Verifies TTLCache integration, context caps, and archival.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'bun:test'
|
|
7
|
+
import fs from 'node:fs/promises'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { ContextBuilder } from '../../agentic/context-builder'
|
|
10
|
+
import { MemorySystem, PatternStore } from '../../agentic/memory-system'
|
|
11
|
+
import pathManager from '../../infrastructure/path-manager'
|
|
12
|
+
import { SessionLogManager } from '../../session/session-log-manager'
|
|
13
|
+
import { TTLCache } from '../../utils/cache'
|
|
14
|
+
|
|
15
|
+
const TEST_GLOBAL_BASE_DIR = path.join(process.cwd(), '.tmp', 'prjct-cli-cache-tests')
|
|
16
|
+
let testCounter = 0
|
|
17
|
+
const getTestProjectId = () => `test-cache-${Date.now()}-${++testCounter}`
|
|
18
|
+
|
|
19
|
+
describe('Cache Eviction Policies (PRJ-288)', () => {
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
pathManager.setGlobalBaseDir(TEST_GLOBAL_BASE_DIR)
|
|
22
|
+
await fs.mkdir(TEST_GLOBAL_BASE_DIR, { recursive: true })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
afterAll(async () => {
|
|
26
|
+
await fs.rm(TEST_GLOBAL_BASE_DIR, { recursive: true, force: true })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// ===========================================================================
|
|
30
|
+
// TTLCache unit tests
|
|
31
|
+
// ===========================================================================
|
|
32
|
+
|
|
33
|
+
describe('TTLCache', () => {
|
|
34
|
+
it('should evict oldest entries when maxSize exceeded', () => {
|
|
35
|
+
const cache = new TTLCache<string>({ maxSize: 3, ttl: 60_000 })
|
|
36
|
+
|
|
37
|
+
cache.set('a', 'val-a')
|
|
38
|
+
cache.set('b', 'val-b')
|
|
39
|
+
cache.set('c', 'val-c')
|
|
40
|
+
expect(cache.size).toBe(3)
|
|
41
|
+
|
|
42
|
+
cache.set('d', 'val-d')
|
|
43
|
+
expect(cache.size).toBe(3)
|
|
44
|
+
// 'a' was oldest, should be evicted
|
|
45
|
+
expect(cache.get('a')).toBeNull()
|
|
46
|
+
expect(cache.get('d')).toBe('val-d')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should expire entries after TTL', async () => {
|
|
50
|
+
const cache = new TTLCache<string>({ maxSize: 10, ttl: 50 })
|
|
51
|
+
|
|
52
|
+
cache.set('key', 'value')
|
|
53
|
+
expect(cache.get('key')).toBe('value')
|
|
54
|
+
|
|
55
|
+
await new Promise((r) => setTimeout(r, 80))
|
|
56
|
+
expect(cache.get('key')).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should prune expired entries', async () => {
|
|
60
|
+
const cache = new TTLCache<string>({ maxSize: 10, ttl: 50 })
|
|
61
|
+
|
|
62
|
+
cache.set('a', '1')
|
|
63
|
+
cache.set('b', '2')
|
|
64
|
+
await new Promise((r) => setTimeout(r, 80))
|
|
65
|
+
|
|
66
|
+
cache.set('c', '3') // fresh
|
|
67
|
+
const pruned = cache.prune()
|
|
68
|
+
expect(pruned).toBe(2)
|
|
69
|
+
expect(cache.size).toBe(1)
|
|
70
|
+
expect(cache.get('c')).toBe('3')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// ===========================================================================
|
|
75
|
+
// SessionLogManager
|
|
76
|
+
// ===========================================================================
|
|
77
|
+
|
|
78
|
+
describe('SessionLogManager', () => {
|
|
79
|
+
it('should use TTLCache with LRU eviction at 50 entries', () => {
|
|
80
|
+
const manager = new SessionLogManager()
|
|
81
|
+
// Verify clearCache works (exercises TTLCache.clear())
|
|
82
|
+
manager.clearCache()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// ===========================================================================
|
|
87
|
+
// ContextBuilder
|
|
88
|
+
// ===========================================================================
|
|
89
|
+
|
|
90
|
+
describe('ContextBuilder', () => {
|
|
91
|
+
it('should report stats from TTLCache', () => {
|
|
92
|
+
const builder = new ContextBuilder()
|
|
93
|
+
const stats = builder.getCacheStats()
|
|
94
|
+
expect(stats).toHaveProperty('size')
|
|
95
|
+
expect(stats).toHaveProperty('maxSize')
|
|
96
|
+
expect(stats).toHaveProperty('ttl')
|
|
97
|
+
expect(stats.maxSize).toBe(200)
|
|
98
|
+
expect(stats.ttl).toBe(5000)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should clear cache and reset projectId', () => {
|
|
102
|
+
const builder = new ContextBuilder()
|
|
103
|
+
builder.clearCache()
|
|
104
|
+
const stats = builder.getCacheStats()
|
|
105
|
+
expect(stats.size).toBe(0)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should invalidate specific cache entries', async () => {
|
|
109
|
+
const builder = new ContextBuilder()
|
|
110
|
+
|
|
111
|
+
// Write a temp file, batchRead to populate cache, then invalidate
|
|
112
|
+
const tmpDir = path.join(TEST_GLOBAL_BASE_DIR, 'ctx-test')
|
|
113
|
+
await fs.mkdir(tmpDir, { recursive: true })
|
|
114
|
+
const tmpFile = path.join(tmpDir, 'test.txt')
|
|
115
|
+
await fs.writeFile(tmpFile, 'hello')
|
|
116
|
+
|
|
117
|
+
const result = await builder.batchRead([tmpFile])
|
|
118
|
+
expect(result.get(tmpFile)).toBe('hello')
|
|
119
|
+
|
|
120
|
+
// Invalidate and verify
|
|
121
|
+
builder.invalidateCache(tmpFile)
|
|
122
|
+
// After invalidation, a fresh batchRead should re-read from disk
|
|
123
|
+
await fs.writeFile(tmpFile, 'updated')
|
|
124
|
+
const result2 = await builder.batchRead([tmpFile])
|
|
125
|
+
expect(result2.get(tmpFile)).toBe('updated')
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// ===========================================================================
|
|
130
|
+
// PatternStore - contexts cap
|
|
131
|
+
// ===========================================================================
|
|
132
|
+
|
|
133
|
+
describe('PatternStore contexts cap', () => {
|
|
134
|
+
let TEST_PROJECT_ID: string
|
|
135
|
+
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
TEST_PROJECT_ID = getTestProjectId()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should cap decision contexts at 20 (FIFO)', async () => {
|
|
141
|
+
const store = new PatternStore()
|
|
142
|
+
|
|
143
|
+
// Record a decision with many unique contexts
|
|
144
|
+
for (let i = 0; i < 25; i++) {
|
|
145
|
+
await store.recordDecision(TEST_PROJECT_ID, 'test-key', 'test-value', `context-${i}`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const patterns = await store.load(TEST_PROJECT_ID)
|
|
149
|
+
const decision = patterns.decisions['test-key']
|
|
150
|
+
expect(decision.contexts.length).toBeLessThanOrEqual(20)
|
|
151
|
+
// Should keep the latest contexts
|
|
152
|
+
expect(decision.contexts).toContain('context-24')
|
|
153
|
+
expect(decision.contexts).toContain('context-5')
|
|
154
|
+
// Oldest should be evicted
|
|
155
|
+
expect(decision.contexts).not.toContain('context-0')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should truncate oversized contexts on afterLoad', async () => {
|
|
159
|
+
const store = new PatternStore()
|
|
160
|
+
const projectId = getTestProjectId()
|
|
161
|
+
|
|
162
|
+
// Manually write a patterns file with oversized contexts
|
|
163
|
+
const filePath = path.join(
|
|
164
|
+
pathManager.getGlobalProjectPath(projectId),
|
|
165
|
+
'memory',
|
|
166
|
+
'patterns.json'
|
|
167
|
+
)
|
|
168
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
169
|
+
|
|
170
|
+
const oversizedPatterns = {
|
|
171
|
+
version: 1,
|
|
172
|
+
decisions: {
|
|
173
|
+
'big-key': {
|
|
174
|
+
value: 'v',
|
|
175
|
+
count: 30,
|
|
176
|
+
firstSeen: '2025-01-01T00:00:00.000Z',
|
|
177
|
+
lastSeen: '2025-06-01T00:00:00.000Z',
|
|
178
|
+
confidence: 'high',
|
|
179
|
+
contexts: Array.from({ length: 50 }, (_, i) => `ctx-${i}`),
|
|
180
|
+
userConfirmed: true,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
preferences: {},
|
|
184
|
+
workflows: {},
|
|
185
|
+
counters: {},
|
|
186
|
+
}
|
|
187
|
+
await fs.writeFile(filePath, JSON.stringify(oversizedPatterns, null, 2), 'utf-8')
|
|
188
|
+
|
|
189
|
+
// Load triggers afterLoad which should truncate
|
|
190
|
+
const patterns = await store.load(projectId)
|
|
191
|
+
expect(patterns.decisions['big-key'].contexts.length).toBe(20)
|
|
192
|
+
// Should keep the latest 20 (indices 30-49)
|
|
193
|
+
expect(patterns.decisions['big-key'].contexts[19]).toBe('ctx-49')
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// ===========================================================================
|
|
198
|
+
// PatternStore - archival
|
|
199
|
+
// ===========================================================================
|
|
200
|
+
|
|
201
|
+
describe('PatternStore archival', () => {
|
|
202
|
+
it('should archive decisions older than 90 days', async () => {
|
|
203
|
+
const store = new PatternStore()
|
|
204
|
+
const projectId = getTestProjectId()
|
|
205
|
+
|
|
206
|
+
// Write patterns with a stale and an active decision
|
|
207
|
+
const filePath = path.join(
|
|
208
|
+
pathManager.getGlobalProjectPath(projectId),
|
|
209
|
+
'memory',
|
|
210
|
+
'patterns.json'
|
|
211
|
+
)
|
|
212
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
213
|
+
|
|
214
|
+
const now = new Date()
|
|
215
|
+
const staleDate = new Date(now.getTime() - 100 * 24 * 60 * 60 * 1000) // 100 days ago
|
|
216
|
+
const recentDate = new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000) // 5 days ago
|
|
217
|
+
|
|
218
|
+
const patterns = {
|
|
219
|
+
version: 1,
|
|
220
|
+
decisions: {
|
|
221
|
+
'stale-key': {
|
|
222
|
+
value: 'old',
|
|
223
|
+
count: 3,
|
|
224
|
+
firstSeen: staleDate.toISOString(),
|
|
225
|
+
lastSeen: staleDate.toISOString(),
|
|
226
|
+
confidence: 'medium',
|
|
227
|
+
contexts: ['ctx1'],
|
|
228
|
+
userConfirmed: false,
|
|
229
|
+
},
|
|
230
|
+
'active-key': {
|
|
231
|
+
value: 'new',
|
|
232
|
+
count: 5,
|
|
233
|
+
firstSeen: recentDate.toISOString(),
|
|
234
|
+
lastSeen: recentDate.toISOString(),
|
|
235
|
+
confidence: 'high',
|
|
236
|
+
contexts: ['ctx2'],
|
|
237
|
+
userConfirmed: true,
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
preferences: {},
|
|
241
|
+
workflows: {},
|
|
242
|
+
counters: {},
|
|
243
|
+
}
|
|
244
|
+
await fs.writeFile(filePath, JSON.stringify(patterns, null, 2), 'utf-8')
|
|
245
|
+
|
|
246
|
+
// Reset store cache to force disk read
|
|
247
|
+
store.reset()
|
|
248
|
+
const archived = await store.archiveStaleDecisions(projectId)
|
|
249
|
+
expect(archived).toBe(1)
|
|
250
|
+
|
|
251
|
+
// Verify active decision remains
|
|
252
|
+
const updated = await store.load(projectId)
|
|
253
|
+
expect(updated.decisions['active-key']).toBeDefined()
|
|
254
|
+
expect(updated.decisions['stale-key']).toBeUndefined()
|
|
255
|
+
|
|
256
|
+
// Verify archive file was created
|
|
257
|
+
const archivePath = path.join(
|
|
258
|
+
pathManager.getGlobalProjectPath(projectId),
|
|
259
|
+
'memory',
|
|
260
|
+
'patterns-archive.json'
|
|
261
|
+
)
|
|
262
|
+
const archiveContent = JSON.parse(await fs.readFile(archivePath, 'utf-8'))
|
|
263
|
+
expect(archiveContent['stale-key']).toBeDefined()
|
|
264
|
+
expect(archiveContent['stale-key'].value).toBe('old')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should return 0 when no stale decisions exist', async () => {
|
|
268
|
+
const store = new PatternStore()
|
|
269
|
+
const projectId = getTestProjectId()
|
|
270
|
+
|
|
271
|
+
// Record a fresh decision
|
|
272
|
+
await store.recordDecision(projectId, 'fresh-key', 'fresh-val', 'ctx')
|
|
273
|
+
const archived = await store.archiveStaleDecisions(projectId)
|
|
274
|
+
expect(archived).toBe(0)
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// ===========================================================================
|
|
279
|
+
// MemorySystem delegation
|
|
280
|
+
// ===========================================================================
|
|
281
|
+
|
|
282
|
+
describe('MemorySystem.archiveStaleDecisions', () => {
|
|
283
|
+
it('should delegate to PatternStore', async () => {
|
|
284
|
+
const ms = new MemorySystem()
|
|
285
|
+
const projectId = getTestProjectId()
|
|
286
|
+
|
|
287
|
+
// Just verify no crash - no stale data to archive
|
|
288
|
+
ms.resetState()
|
|
289
|
+
await ms.recordDecision(projectId, 'key', 'val', 'ctx')
|
|
290
|
+
const count = await ms.archiveStaleDecisions(projectId)
|
|
291
|
+
expect(count).toBe(0)
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
})
|
|
@@ -11,6 +11,7 @@ import configManager from '../infrastructure/config-manager'
|
|
|
11
11
|
import pathManager from '../infrastructure/path-manager'
|
|
12
12
|
import type { ContextPaths, ContextState, ProjectContext } from '../types'
|
|
13
13
|
import { isNotFoundError } from '../types/fs'
|
|
14
|
+
import { TTLCache } from '../utils/cache'
|
|
14
15
|
|
|
15
16
|
// Re-export types for convenience
|
|
16
17
|
export type { ContextPaths, ContextState, ProjectContext } from '../types'
|
|
@@ -20,34 +21,22 @@ export type Paths = ContextPaths
|
|
|
20
21
|
export type Context = ProjectContext
|
|
21
22
|
export type State = ContextState
|
|
22
23
|
|
|
24
|
+
interface CachedFile {
|
|
25
|
+
content: string | null
|
|
26
|
+
mtime: number | null
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
/**
|
|
24
30
|
* Builds and caches project context for Claude decisions.
|
|
25
31
|
* Features parallel reads, selective loading, and anti-hallucination mtime checks.
|
|
26
32
|
*/
|
|
27
33
|
class ContextBuilder {
|
|
28
|
-
private _cache:
|
|
29
|
-
private
|
|
30
|
-
private _lastCacheTime: number | null
|
|
31
|
-
private _mtimes: Map<string, number>
|
|
34
|
+
private _cache: TTLCache<CachedFile>
|
|
35
|
+
private _currentProjectId: string | null
|
|
32
36
|
|
|
33
37
|
constructor() {
|
|
34
|
-
|
|
35
|
-
this.
|
|
36
|
-
// ANTI-HALLUCINATION: Reduced from 30s to 5s to prevent stale data
|
|
37
|
-
this._cacheTimeout = 5000 // 5 seconds (was 30s - caused stale context issues)
|
|
38
|
-
this._lastCacheTime = null
|
|
39
|
-
// Track file modification times for additional staleness detection
|
|
40
|
-
this._mtimes = new Map()
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Clear cache if stale or force clear
|
|
45
|
-
*/
|
|
46
|
-
private _clearCacheIfStale(force: boolean = false): void {
|
|
47
|
-
if (force || !this._lastCacheTime || Date.now() - this._lastCacheTime > this._cacheTimeout) {
|
|
48
|
-
this._cache.clear()
|
|
49
|
-
this._lastCacheTime = Date.now()
|
|
50
|
-
}
|
|
38
|
+
this._cache = new TTLCache<CachedFile>({ ttl: 5000, maxSize: 200 })
|
|
39
|
+
this._currentProjectId = null
|
|
51
40
|
}
|
|
52
41
|
|
|
53
42
|
/**
|
|
@@ -57,6 +46,12 @@ class ContextBuilder {
|
|
|
57
46
|
const projectId = await configManager.getProjectId(projectPath)
|
|
58
47
|
const globalPath = pathManager.getGlobalProjectPath(projectId!)
|
|
59
48
|
|
|
49
|
+
// Clear cache on project switch
|
|
50
|
+
if (this._currentProjectId !== null && this._currentProjectId !== projectId) {
|
|
51
|
+
this._cache.clear()
|
|
52
|
+
}
|
|
53
|
+
this._currentProjectId = projectId
|
|
54
|
+
|
|
60
55
|
return {
|
|
61
56
|
// Project identification
|
|
62
57
|
projectId,
|
|
@@ -94,8 +89,6 @@ class ContextBuilder {
|
|
|
94
89
|
* Uses Promise.all() for 40-60% faster file I/O
|
|
95
90
|
*/
|
|
96
91
|
async loadState(context: Context, onlyKeys: string[] | null = null): Promise<State> {
|
|
97
|
-
this._clearCacheIfStale()
|
|
98
|
-
|
|
99
92
|
const state: State = {}
|
|
100
93
|
const entries = Object.entries(context.paths)
|
|
101
94
|
|
|
@@ -105,20 +98,16 @@ class ContextBuilder {
|
|
|
105
98
|
// ANTI-HALLUCINATION: Verify mtime before trusting cache
|
|
106
99
|
// Files can change between commands - stale cache causes hallucinations
|
|
107
100
|
for (const [, filePath] of filteredEntries) {
|
|
108
|
-
|
|
101
|
+
const cachedEntry = this._cache.get(filePath)
|
|
102
|
+
if (cachedEntry !== null) {
|
|
109
103
|
try {
|
|
110
104
|
const stat = await fs.stat(filePath)
|
|
111
|
-
|
|
112
|
-
if (!cachedMtime || stat.mtimeMs > cachedMtime) {
|
|
113
|
-
// File changed since cached - invalidate
|
|
105
|
+
if (!cachedEntry.mtime || stat.mtimeMs > cachedEntry.mtime) {
|
|
114
106
|
this._cache.delete(filePath)
|
|
115
|
-
this._mtimes.delete(filePath)
|
|
116
107
|
}
|
|
117
108
|
} catch (error) {
|
|
118
|
-
// File doesn't exist or access error - invalidate cache
|
|
119
109
|
if (isNotFoundError(error)) {
|
|
120
110
|
this._cache.delete(filePath)
|
|
121
|
-
this._mtimes.delete(filePath)
|
|
122
111
|
} else {
|
|
123
112
|
throw error
|
|
124
113
|
}
|
|
@@ -129,8 +118,9 @@ class ContextBuilder {
|
|
|
129
118
|
// Separate cached vs uncached files
|
|
130
119
|
const uncachedEntries: [string, string][] = []
|
|
131
120
|
for (const [key, filePath] of filteredEntries) {
|
|
132
|
-
|
|
133
|
-
|
|
121
|
+
const cachedEntry = this._cache.get(filePath)
|
|
122
|
+
if (cachedEntry !== null) {
|
|
123
|
+
state[key] = cachedEntry.content
|
|
134
124
|
} else {
|
|
135
125
|
uncachedEntries.push([key, filePath])
|
|
136
126
|
}
|
|
@@ -155,13 +145,9 @@ class ContextBuilder {
|
|
|
155
145
|
|
|
156
146
|
const results = await Promise.all(readPromises)
|
|
157
147
|
|
|
158
|
-
// Populate state and cache (with mtime for anti-hallucination)
|
|
159
148
|
for (const { key, filePath, content, mtime } of results) {
|
|
160
149
|
state[key] = content
|
|
161
|
-
this._cache.set(filePath, content)
|
|
162
|
-
if (mtime) {
|
|
163
|
-
this._mtimes.set(filePath, mtime)
|
|
164
|
-
}
|
|
150
|
+
this._cache.set(filePath, { content, mtime })
|
|
165
151
|
}
|
|
166
152
|
}
|
|
167
153
|
|
|
@@ -218,15 +204,14 @@ class ContextBuilder {
|
|
|
218
204
|
* Utility for custom file sets
|
|
219
205
|
*/
|
|
220
206
|
async batchRead(filePaths: string[]): Promise<Map<string, string | null>> {
|
|
221
|
-
this._clearCacheIfStale()
|
|
222
|
-
|
|
223
207
|
const results = new Map<string, string | null>()
|
|
224
208
|
const uncachedPaths: string[] = []
|
|
225
209
|
|
|
226
210
|
// Check cache first
|
|
227
211
|
for (const filePath of filePaths) {
|
|
228
|
-
|
|
229
|
-
|
|
212
|
+
const cachedEntry = this._cache.get(filePath)
|
|
213
|
+
if (cachedEntry !== null) {
|
|
214
|
+
results.set(filePath, cachedEntry.content)
|
|
230
215
|
} else {
|
|
231
216
|
uncachedPaths.push(filePath)
|
|
232
217
|
}
|
|
@@ -250,7 +235,7 @@ class ContextBuilder {
|
|
|
250
235
|
|
|
251
236
|
for (const { filePath, content } of readResults) {
|
|
252
237
|
results.set(filePath, content)
|
|
253
|
-
this._cache.set(filePath, content)
|
|
238
|
+
this._cache.set(filePath, { content, mtime: null })
|
|
254
239
|
}
|
|
255
240
|
}
|
|
256
241
|
|
|
@@ -268,7 +253,8 @@ class ContextBuilder {
|
|
|
268
253
|
* Force clear entire cache
|
|
269
254
|
*/
|
|
270
255
|
clearCache(): void {
|
|
271
|
-
this.
|
|
256
|
+
this._cache.clear()
|
|
257
|
+
this._currentProjectId = null
|
|
272
258
|
}
|
|
273
259
|
|
|
274
260
|
/**
|
|
@@ -289,12 +275,8 @@ class ContextBuilder {
|
|
|
289
275
|
/**
|
|
290
276
|
* Get cache stats (for debugging/metrics)
|
|
291
277
|
*/
|
|
292
|
-
getCacheStats(): { size: number;
|
|
293
|
-
return
|
|
294
|
-
size: this._cache.size,
|
|
295
|
-
lastRefresh: this._lastCacheTime,
|
|
296
|
-
timeout: this._cacheTimeout,
|
|
297
|
-
}
|
|
278
|
+
getCacheStats(): { size: number; maxSize: number; ttl: number } {
|
|
279
|
+
return this._cache.stats()
|
|
298
280
|
}
|
|
299
281
|
}
|
|
300
282
|
|
|
@@ -351,6 +351,9 @@ export class HistoryStore {
|
|
|
351
351
|
* Persistent learned preferences and decisions.
|
|
352
352
|
*/
|
|
353
353
|
export class PatternStore extends CachedStore<Patterns> {
|
|
354
|
+
private static readonly MAX_CONTEXTS = 20
|
|
355
|
+
private static readonly ARCHIVE_AGE_DAYS = 90
|
|
356
|
+
|
|
354
357
|
protected getFilename(): string {
|
|
355
358
|
return 'patterns.json'
|
|
356
359
|
}
|
|
@@ -365,6 +368,14 @@ export class PatternStore extends CachedStore<Patterns> {
|
|
|
365
368
|
}
|
|
366
369
|
}
|
|
367
370
|
|
|
371
|
+
protected afterLoad(patterns: Patterns): void {
|
|
372
|
+
for (const decision of Object.values(patterns.decisions)) {
|
|
373
|
+
if (decision.contexts.length > PatternStore.MAX_CONTEXTS) {
|
|
374
|
+
decision.contexts = decision.contexts.slice(-PatternStore.MAX_CONTEXTS)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
368
379
|
// Convenience alias for backward compatibility
|
|
369
380
|
async loadPatterns(projectId: string): Promise<Patterns> {
|
|
370
381
|
return this.load(projectId)
|
|
@@ -404,6 +415,9 @@ export class PatternStore extends CachedStore<Patterns> {
|
|
|
404
415
|
decision.lastSeen = now
|
|
405
416
|
if (context && !decision.contexts.includes(context)) {
|
|
406
417
|
decision.contexts.push(context)
|
|
418
|
+
if (decision.contexts.length > PatternStore.MAX_CONTEXTS) {
|
|
419
|
+
decision.contexts = decision.contexts.slice(-PatternStore.MAX_CONTEXTS)
|
|
420
|
+
}
|
|
407
421
|
}
|
|
408
422
|
if (options.userConfirmed) {
|
|
409
423
|
decision.userConfirmed = true
|
|
@@ -553,6 +567,50 @@ export class PatternStore extends CachedStore<Patterns> {
|
|
|
553
567
|
preferences: Object.keys(patterns.preferences).length,
|
|
554
568
|
}
|
|
555
569
|
}
|
|
570
|
+
|
|
571
|
+
private _getArchivePath(projectId: string): string {
|
|
572
|
+
const basePath = path.join(pathManager.getGlobalProjectPath(projectId), 'memory')
|
|
573
|
+
return path.join(basePath, 'patterns-archive.json')
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async archiveStaleDecisions(projectId: string): Promise<number> {
|
|
577
|
+
const patterns = await this.load(projectId)
|
|
578
|
+
const now = Date.now()
|
|
579
|
+
const cutoff = PatternStore.ARCHIVE_AGE_DAYS * 24 * 60 * 60 * 1000
|
|
580
|
+
|
|
581
|
+
const staleKeys: string[] = []
|
|
582
|
+
for (const [key, decision] of Object.entries(patterns.decisions)) {
|
|
583
|
+
const lastSeenMs = new Date(decision.lastSeen).getTime()
|
|
584
|
+
if (now - lastSeenMs > cutoff) {
|
|
585
|
+
staleKeys.push(key)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (staleKeys.length === 0) return 0
|
|
590
|
+
|
|
591
|
+
// Load or create archive
|
|
592
|
+
const archivePath = this._getArchivePath(projectId)
|
|
593
|
+
let archive: Record<string, unknown> = {}
|
|
594
|
+
try {
|
|
595
|
+
const content = await fs.readFile(archivePath, 'utf-8')
|
|
596
|
+
archive = JSON.parse(content)
|
|
597
|
+
} catch (error) {
|
|
598
|
+
if (!isNotFoundError(error)) throw error
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Move stale decisions to archive
|
|
602
|
+
for (const key of staleKeys) {
|
|
603
|
+
archive[key] = patterns.decisions[key]
|
|
604
|
+
delete patterns.decisions[key]
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Save archive and pruned patterns
|
|
608
|
+
await fs.mkdir(path.dirname(archivePath), { recursive: true })
|
|
609
|
+
await fs.writeFile(archivePath, JSON.stringify(archive, null, 2), 'utf-8')
|
|
610
|
+
await this.save(projectId)
|
|
611
|
+
|
|
612
|
+
return staleKeys.length
|
|
613
|
+
}
|
|
556
614
|
}
|
|
557
615
|
|
|
558
616
|
// =============================================================================
|
|
@@ -1226,6 +1284,10 @@ export class MemorySystem {
|
|
|
1226
1284
|
return this._patternStore.getPatternsSummary(projectId)
|
|
1227
1285
|
}
|
|
1228
1286
|
|
|
1287
|
+
archiveStaleDecisions(projectId: string): Promise<number> {
|
|
1288
|
+
return this._patternStore.archiveStaleDecisions(projectId)
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1229
1291
|
// ===========================================================================
|
|
1230
1292
|
// TIER 3: History
|
|
1231
1293
|
// ===========================================================================
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Implements IssueTrackerProvider for Linear using @linear/sdk
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { LinearClient as LinearSDK } from '@linear/sdk'
|
|
6
|
+
import type { LinearClient as LinearSDK } from '@linear/sdk'
|
|
7
7
|
import { getErrorMessage } from '../../types/fs'
|
|
8
8
|
import { getCredential } from '../../utils/keychain'
|
|
9
9
|
import type {
|
|
@@ -78,7 +78,8 @@ export class LinearProvider implements IssueTrackerProvider {
|
|
|
78
78
|
throw new Error('LINEAR_API_KEY not configured. Run `p. linear setup` to configure.')
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
const { LinearClient } = await import('@linear/sdk')
|
|
82
|
+
this.sdk = new LinearClient({ apiKey })
|
|
82
83
|
|
|
83
84
|
// Verify connection silently (no output noise)
|
|
84
85
|
try {
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
SessionStats,
|
|
14
14
|
} from '../types'
|
|
15
15
|
import { getErrorMessage } from '../types/fs'
|
|
16
|
+
import { TTLCache } from '../utils/cache'
|
|
16
17
|
import * as dateHelper from '../utils/date-helper'
|
|
17
18
|
import * as fileHelper from '../utils/file-helper'
|
|
18
19
|
import * as jsonlHelper from '../utils/jsonl-helper'
|
|
@@ -20,12 +21,12 @@ import { VERSION } from '../utils/version'
|
|
|
20
21
|
import { migrateLegacyJsonl, migrateLegacyMarkdown } from './log-migration'
|
|
21
22
|
|
|
22
23
|
export class SessionLogManager {
|
|
23
|
-
private currentSessionCache:
|
|
24
|
-
private sessionMetadataCache:
|
|
24
|
+
private currentSessionCache: TTLCache<string>
|
|
25
|
+
private sessionMetadataCache: TTLCache<SessionLogMetadata>
|
|
25
26
|
|
|
26
27
|
constructor() {
|
|
27
|
-
this.currentSessionCache = new
|
|
28
|
-
this.sessionMetadataCache = new
|
|
28
|
+
this.currentSessionCache = new TTLCache<string>({ maxSize: 50, ttl: 3_600_000 })
|
|
29
|
+
this.sessionMetadataCache = new TTLCache<SessionLogMetadata>({ maxSize: 50, ttl: 3_600_000 })
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -34,8 +35,9 @@ export class SessionLogManager {
|
|
|
34
35
|
async getCurrentSession(projectId: string): Promise<string> {
|
|
35
36
|
const cacheKey = `${projectId}-${this._getTodayKey()}`
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
const cached = this.currentSessionCache.get(cacheKey)
|
|
39
|
+
if (cached !== null) {
|
|
40
|
+
return cached
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
const sessionPath = await pathManager.ensureSessionPath(projectId)
|
|
@@ -243,8 +245,9 @@ export class SessionLogManager {
|
|
|
243
245
|
private async _getSessionLogMetadata(sessionPath: string): Promise<SessionLogMetadata | null> {
|
|
244
246
|
const metadataPath = path.join(sessionPath, 'session-meta.json')
|
|
245
247
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
+
const cached = this.sessionMetadataCache.get(sessionPath)
|
|
249
|
+
if (cached !== null) {
|
|
250
|
+
return cached
|
|
248
251
|
}
|
|
249
252
|
|
|
250
253
|
const metadata = await fileHelper.readJson<SessionLogMetadata>(metadataPath, null)
|
package/dist/bin/prjct.mjs
CHANGED
|
@@ -7572,28 +7572,16 @@ var init_context_builder = __esm({
|
|
|
7572
7572
|
init_config_manager();
|
|
7573
7573
|
init_path_manager();
|
|
7574
7574
|
init_fs();
|
|
7575
|
+
init_cache();
|
|
7575
7576
|
ContextBuilder = class {
|
|
7576
7577
|
static {
|
|
7577
7578
|
__name(this, "ContextBuilder");
|
|
7578
7579
|
}
|
|
7579
7580
|
_cache;
|
|
7580
|
-
|
|
7581
|
-
_lastCacheTime;
|
|
7582
|
-
_mtimes;
|
|
7581
|
+
_currentProjectId;
|
|
7583
7582
|
constructor() {
|
|
7584
|
-
this._cache =
|
|
7585
|
-
this.
|
|
7586
|
-
this._lastCacheTime = null;
|
|
7587
|
-
this._mtimes = /* @__PURE__ */ new Map();
|
|
7588
|
-
}
|
|
7589
|
-
/**
|
|
7590
|
-
* Clear cache if stale or force clear
|
|
7591
|
-
*/
|
|
7592
|
-
_clearCacheIfStale(force = false) {
|
|
7593
|
-
if (force || !this._lastCacheTime || Date.now() - this._lastCacheTime > this._cacheTimeout) {
|
|
7594
|
-
this._cache.clear();
|
|
7595
|
-
this._lastCacheTime = Date.now();
|
|
7596
|
-
}
|
|
7583
|
+
this._cache = new TTLCache({ ttl: 5e3, maxSize: 200 });
|
|
7584
|
+
this._currentProjectId = null;
|
|
7597
7585
|
}
|
|
7598
7586
|
/**
|
|
7599
7587
|
* Build full project context for Claude
|
|
@@ -7601,6 +7589,10 @@ var init_context_builder = __esm({
|
|
|
7601
7589
|
async build(projectPath, commandParams = {}) {
|
|
7602
7590
|
const projectId = await config_manager_default.getProjectId(projectPath);
|
|
7603
7591
|
const globalPath = path_manager_default.getGlobalProjectPath(projectId);
|
|
7592
|
+
if (this._currentProjectId !== null && this._currentProjectId !== projectId) {
|
|
7593
|
+
this._cache.clear();
|
|
7594
|
+
}
|
|
7595
|
+
this._currentProjectId = projectId;
|
|
7604
7596
|
return {
|
|
7605
7597
|
// Project identification
|
|
7606
7598
|
projectId,
|
|
@@ -7636,23 +7628,20 @@ var init_context_builder = __esm({
|
|
|
7636
7628
|
* Uses Promise.all() for 40-60% faster file I/O
|
|
7637
7629
|
*/
|
|
7638
7630
|
async loadState(context2, onlyKeys = null) {
|
|
7639
|
-
this._clearCacheIfStale();
|
|
7640
7631
|
const state = {};
|
|
7641
7632
|
const entries = Object.entries(context2.paths);
|
|
7642
7633
|
const filteredEntries = onlyKeys ? entries.filter(([key]) => onlyKeys.includes(key)) : entries;
|
|
7643
7634
|
for (const [, filePath] of filteredEntries) {
|
|
7644
|
-
|
|
7635
|
+
const cachedEntry = this._cache.get(filePath);
|
|
7636
|
+
if (cachedEntry !== null) {
|
|
7645
7637
|
try {
|
|
7646
7638
|
const stat = await fs21.stat(filePath);
|
|
7647
|
-
|
|
7648
|
-
if (!cachedMtime || stat.mtimeMs > cachedMtime) {
|
|
7639
|
+
if (!cachedEntry.mtime || stat.mtimeMs > cachedEntry.mtime) {
|
|
7649
7640
|
this._cache.delete(filePath);
|
|
7650
|
-
this._mtimes.delete(filePath);
|
|
7651
7641
|
}
|
|
7652
7642
|
} catch (error) {
|
|
7653
7643
|
if (isNotFoundError(error)) {
|
|
7654
7644
|
this._cache.delete(filePath);
|
|
7655
|
-
this._mtimes.delete(filePath);
|
|
7656
7645
|
} else {
|
|
7657
7646
|
throw error;
|
|
7658
7647
|
}
|
|
@@ -7661,8 +7650,9 @@ var init_context_builder = __esm({
|
|
|
7661
7650
|
}
|
|
7662
7651
|
const uncachedEntries = [];
|
|
7663
7652
|
for (const [key, filePath] of filteredEntries) {
|
|
7664
|
-
|
|
7665
|
-
|
|
7653
|
+
const cachedEntry = this._cache.get(filePath);
|
|
7654
|
+
if (cachedEntry !== null) {
|
|
7655
|
+
state[key] = cachedEntry.content;
|
|
7666
7656
|
} else {
|
|
7667
7657
|
uncachedEntries.push([key, filePath]);
|
|
7668
7658
|
}
|
|
@@ -7685,10 +7675,7 @@ var init_context_builder = __esm({
|
|
|
7685
7675
|
const results = await Promise.all(readPromises);
|
|
7686
7676
|
for (const { key, filePath, content, mtime } of results) {
|
|
7687
7677
|
state[key] = content;
|
|
7688
|
-
this._cache.set(filePath, content);
|
|
7689
|
-
if (mtime) {
|
|
7690
|
-
this._mtimes.set(filePath, mtime);
|
|
7691
|
-
}
|
|
7678
|
+
this._cache.set(filePath, { content, mtime });
|
|
7692
7679
|
}
|
|
7693
7680
|
}
|
|
7694
7681
|
return state;
|
|
@@ -7733,12 +7720,12 @@ var init_context_builder = __esm({
|
|
|
7733
7720
|
* Utility for custom file sets
|
|
7734
7721
|
*/
|
|
7735
7722
|
async batchRead(filePaths) {
|
|
7736
|
-
this._clearCacheIfStale();
|
|
7737
7723
|
const results = /* @__PURE__ */ new Map();
|
|
7738
7724
|
const uncachedPaths = [];
|
|
7739
7725
|
for (const filePath of filePaths) {
|
|
7740
|
-
|
|
7741
|
-
|
|
7726
|
+
const cachedEntry = this._cache.get(filePath);
|
|
7727
|
+
if (cachedEntry !== null) {
|
|
7728
|
+
results.set(filePath, cachedEntry.content);
|
|
7742
7729
|
} else {
|
|
7743
7730
|
uncachedPaths.push(filePath);
|
|
7744
7731
|
}
|
|
@@ -7758,7 +7745,7 @@ var init_context_builder = __esm({
|
|
|
7758
7745
|
const readResults = await Promise.all(readPromises);
|
|
7759
7746
|
for (const { filePath, content } of readResults) {
|
|
7760
7747
|
results.set(filePath, content);
|
|
7761
|
-
this._cache.set(filePath, content);
|
|
7748
|
+
this._cache.set(filePath, { content, mtime: null });
|
|
7762
7749
|
}
|
|
7763
7750
|
}
|
|
7764
7751
|
return results;
|
|
@@ -7773,7 +7760,8 @@ var init_context_builder = __esm({
|
|
|
7773
7760
|
* Force clear entire cache
|
|
7774
7761
|
*/
|
|
7775
7762
|
clearCache() {
|
|
7776
|
-
this.
|
|
7763
|
+
this._cache.clear();
|
|
7764
|
+
this._currentProjectId = null;
|
|
7777
7765
|
}
|
|
7778
7766
|
/**
|
|
7779
7767
|
* Check file existence
|
|
@@ -7793,11 +7781,7 @@ var init_context_builder = __esm({
|
|
|
7793
7781
|
* Get cache stats (for debugging/metrics)
|
|
7794
7782
|
*/
|
|
7795
7783
|
getCacheStats() {
|
|
7796
|
-
return
|
|
7797
|
-
size: this._cache.size,
|
|
7798
|
-
lastRefresh: this._lastCacheTime,
|
|
7799
|
-
timeout: this._cacheTimeout
|
|
7800
|
-
};
|
|
7784
|
+
return this._cache.stats();
|
|
7801
7785
|
}
|
|
7802
7786
|
};
|
|
7803
7787
|
contextBuilder = new ContextBuilder();
|
|
@@ -10094,10 +10078,12 @@ var init_memory_system = __esm({
|
|
|
10094
10078
|
return getLastJsonLines(sessionPath, limit);
|
|
10095
10079
|
}
|
|
10096
10080
|
};
|
|
10097
|
-
PatternStore = class extends CachedStore {
|
|
10081
|
+
PatternStore = class _PatternStore extends CachedStore {
|
|
10098
10082
|
static {
|
|
10099
10083
|
__name(this, "PatternStore");
|
|
10100
10084
|
}
|
|
10085
|
+
static MAX_CONTEXTS = 20;
|
|
10086
|
+
static ARCHIVE_AGE_DAYS = 90;
|
|
10101
10087
|
getFilename() {
|
|
10102
10088
|
return "patterns.json";
|
|
10103
10089
|
}
|
|
@@ -10110,6 +10096,13 @@ var init_memory_system = __esm({
|
|
|
10110
10096
|
counters: {}
|
|
10111
10097
|
};
|
|
10112
10098
|
}
|
|
10099
|
+
afterLoad(patterns) {
|
|
10100
|
+
for (const decision of Object.values(patterns.decisions)) {
|
|
10101
|
+
if (decision.contexts.length > _PatternStore.MAX_CONTEXTS) {
|
|
10102
|
+
decision.contexts = decision.contexts.slice(-_PatternStore.MAX_CONTEXTS);
|
|
10103
|
+
}
|
|
10104
|
+
}
|
|
10105
|
+
}
|
|
10113
10106
|
// Convenience alias for backward compatibility
|
|
10114
10107
|
async loadPatterns(projectId) {
|
|
10115
10108
|
return this.load(projectId);
|
|
@@ -10137,6 +10130,9 @@ var init_memory_system = __esm({
|
|
|
10137
10130
|
decision.lastSeen = now;
|
|
10138
10131
|
if (context2 && !decision.contexts.includes(context2)) {
|
|
10139
10132
|
decision.contexts.push(context2);
|
|
10133
|
+
if (decision.contexts.length > _PatternStore.MAX_CONTEXTS) {
|
|
10134
|
+
decision.contexts = decision.contexts.slice(-_PatternStore.MAX_CONTEXTS);
|
|
10135
|
+
}
|
|
10140
10136
|
}
|
|
10141
10137
|
if (options.userConfirmed) {
|
|
10142
10138
|
decision.userConfirmed = true;
|
|
@@ -10246,6 +10242,39 @@ var init_memory_system = __esm({
|
|
|
10246
10242
|
preferences: Object.keys(patterns.preferences).length
|
|
10247
10243
|
};
|
|
10248
10244
|
}
|
|
10245
|
+
_getArchivePath(projectId) {
|
|
10246
|
+
const basePath = path22.join(path_manager_default.getGlobalProjectPath(projectId), "memory");
|
|
10247
|
+
return path22.join(basePath, "patterns-archive.json");
|
|
10248
|
+
}
|
|
10249
|
+
async archiveStaleDecisions(projectId) {
|
|
10250
|
+
const patterns = await this.load(projectId);
|
|
10251
|
+
const now = Date.now();
|
|
10252
|
+
const cutoff = _PatternStore.ARCHIVE_AGE_DAYS * 24 * 60 * 60 * 1e3;
|
|
10253
|
+
const staleKeys = [];
|
|
10254
|
+
for (const [key, decision] of Object.entries(patterns.decisions)) {
|
|
10255
|
+
const lastSeenMs = new Date(decision.lastSeen).getTime();
|
|
10256
|
+
if (now - lastSeenMs > cutoff) {
|
|
10257
|
+
staleKeys.push(key);
|
|
10258
|
+
}
|
|
10259
|
+
}
|
|
10260
|
+
if (staleKeys.length === 0) return 0;
|
|
10261
|
+
const archivePath = this._getArchivePath(projectId);
|
|
10262
|
+
let archive = {};
|
|
10263
|
+
try {
|
|
10264
|
+
const content = await fs24.readFile(archivePath, "utf-8");
|
|
10265
|
+
archive = JSON.parse(content);
|
|
10266
|
+
} catch (error) {
|
|
10267
|
+
if (!isNotFoundError(error)) throw error;
|
|
10268
|
+
}
|
|
10269
|
+
for (const key of staleKeys) {
|
|
10270
|
+
archive[key] = patterns.decisions[key];
|
|
10271
|
+
delete patterns.decisions[key];
|
|
10272
|
+
}
|
|
10273
|
+
await fs24.mkdir(path22.dirname(archivePath), { recursive: true });
|
|
10274
|
+
await fs24.writeFile(archivePath, JSON.stringify(archive, null, 2), "utf-8");
|
|
10275
|
+
await this.save(projectId);
|
|
10276
|
+
return staleKeys.length;
|
|
10277
|
+
}
|
|
10249
10278
|
};
|
|
10250
10279
|
SemanticMemories = class extends CachedStore {
|
|
10251
10280
|
static {
|
|
@@ -10727,6 +10756,9 @@ Context: ${context2}` : ""}`,
|
|
|
10727
10756
|
getPatternsSummary(projectId) {
|
|
10728
10757
|
return this._patternStore.getPatternsSummary(projectId);
|
|
10729
10758
|
}
|
|
10759
|
+
archiveStaleDecisions(projectId) {
|
|
10760
|
+
return this._patternStore.archiveStaleDecisions(projectId);
|
|
10761
|
+
}
|
|
10730
10762
|
// ===========================================================================
|
|
10731
10763
|
// TIER 3: History
|
|
10732
10764
|
// ===========================================================================
|
|
@@ -27758,7 +27790,6 @@ var init_keychain = __esm({
|
|
|
27758
27790
|
});
|
|
27759
27791
|
|
|
27760
27792
|
// core/integrations/linear/client.ts
|
|
27761
|
-
import { LinearClient as LinearSDK } from "@linear/sdk";
|
|
27762
27793
|
var LINEAR_STATUS_MAP, LINEAR_PRIORITY_MAP, PRIORITY_TO_LINEAR, LinearProvider, linearProvider;
|
|
27763
27794
|
var init_client = __esm({
|
|
27764
27795
|
"core/integrations/linear/client.ts"() {
|
|
@@ -27811,7 +27842,8 @@ var init_client = __esm({
|
|
|
27811
27842
|
if (!apiKey) {
|
|
27812
27843
|
throw new Error("LINEAR_API_KEY not configured. Run `p. linear setup` to configure.");
|
|
27813
27844
|
}
|
|
27814
|
-
|
|
27845
|
+
const { LinearClient } = await import("@linear/sdk");
|
|
27846
|
+
this.sdk = new LinearClient({ apiKey });
|
|
27815
27847
|
try {
|
|
27816
27848
|
await this.sdk.viewer;
|
|
27817
27849
|
} catch (error) {
|
|
@@ -29110,7 +29142,7 @@ var require_package = __commonJS({
|
|
|
29110
29142
|
"package.json"(exports, module) {
|
|
29111
29143
|
module.exports = {
|
|
29112
29144
|
name: "prjct-cli",
|
|
29113
|
-
version: "1.7.
|
|
29145
|
+
version: "1.7.5",
|
|
29114
29146
|
description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
|
|
29115
29147
|
main: "core/index.ts",
|
|
29116
29148
|
bin: {
|
|
@@ -29166,11 +29198,9 @@ var require_package = __commonJS({
|
|
|
29166
29198
|
chalk: "^4.1.2",
|
|
29167
29199
|
chokidar: "^5.0.0",
|
|
29168
29200
|
"date-fns": "^4.1.0",
|
|
29169
|
-
esbuild: "^0.25.0",
|
|
29170
29201
|
glob: "^13.0.1",
|
|
29171
29202
|
hono: "^4.11.3",
|
|
29172
29203
|
"jsonc-parser": "^3.3.1",
|
|
29173
|
-
lightningcss: "^1.30.2",
|
|
29174
29204
|
prompts: "^2.4.2",
|
|
29175
29205
|
zod: "^3.24.1"
|
|
29176
29206
|
},
|
|
@@ -29179,6 +29209,7 @@ var require_package = __commonJS({
|
|
|
29179
29209
|
"@types/bun": "latest",
|
|
29180
29210
|
"@types/chokidar": "^2.1.7",
|
|
29181
29211
|
"@types/prompts": "^2.4.9",
|
|
29212
|
+
esbuild: "^0.25.0",
|
|
29182
29213
|
lefthook: "^2.1.0",
|
|
29183
29214
|
typescript: "^5.9.3"
|
|
29184
29215
|
},
|
|
@@ -29202,7 +29233,6 @@ var require_package = __commonJS({
|
|
|
29202
29233
|
"dist/",
|
|
29203
29234
|
"templates/",
|
|
29204
29235
|
"scripts/postinstall.js",
|
|
29205
|
-
"scripts/build.js",
|
|
29206
29236
|
"scripts/install.sh",
|
|
29207
29237
|
"LICENSE",
|
|
29208
29238
|
"README.md",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prjct-cli",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.5",
|
|
4
4
|
"description": "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
|
|
5
5
|
"main": "core/index.ts",
|
|
6
6
|
"bin": {
|
|
@@ -56,11 +56,9 @@
|
|
|
56
56
|
"chalk": "^4.1.2",
|
|
57
57
|
"chokidar": "^5.0.0",
|
|
58
58
|
"date-fns": "^4.1.0",
|
|
59
|
-
"esbuild": "^0.25.0",
|
|
60
59
|
"glob": "^13.0.1",
|
|
61
60
|
"hono": "^4.11.3",
|
|
62
61
|
"jsonc-parser": "^3.3.1",
|
|
63
|
-
"lightningcss": "^1.30.2",
|
|
64
62
|
"prompts": "^2.4.2",
|
|
65
63
|
"zod": "^3.24.1"
|
|
66
64
|
},
|
|
@@ -69,6 +67,7 @@
|
|
|
69
67
|
"@types/bun": "latest",
|
|
70
68
|
"@types/chokidar": "^2.1.7",
|
|
71
69
|
"@types/prompts": "^2.4.9",
|
|
70
|
+
"esbuild": "^0.25.0",
|
|
72
71
|
"lefthook": "^2.1.0",
|
|
73
72
|
"typescript": "^5.9.3"
|
|
74
73
|
},
|
|
@@ -92,7 +91,6 @@
|
|
|
92
91
|
"dist/",
|
|
93
92
|
"templates/",
|
|
94
93
|
"scripts/postinstall.js",
|
|
95
|
-
"scripts/build.js",
|
|
96
94
|
"scripts/install.sh",
|
|
97
95
|
"LICENSE",
|
|
98
96
|
"README.md",
|
package/scripts/build.js
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Build Script for prjct-cli
|
|
5
|
-
*
|
|
6
|
-
* Compiles TypeScript to JavaScript for Node.js compatibility.
|
|
7
|
-
* Builds:
|
|
8
|
-
* - bin/prjct.mjs (CLI entry point)
|
|
9
|
-
* - core/infrastructure/setup.js (postinstall needs this)
|
|
10
|
-
* - core/infrastructure/command-installer.js
|
|
11
|
-
* - core/infrastructure/editors-config.js
|
|
12
|
-
* - core/utils/version.js
|
|
13
|
-
*
|
|
14
|
-
* @version 2.0.0
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const { execSync } = require('node:child_process')
|
|
18
|
-
const fs = require('node:fs')
|
|
19
|
-
const path = require('node:path')
|
|
20
|
-
|
|
21
|
-
const ROOT = path.resolve(__dirname, '..')
|
|
22
|
-
const DIST = path.join(ROOT, 'dist')
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Ensure esbuild is available
|
|
26
|
-
*/
|
|
27
|
-
function ensureEsbuild() {
|
|
28
|
-
try {
|
|
29
|
-
require.resolve('esbuild')
|
|
30
|
-
return true
|
|
31
|
-
} catch {
|
|
32
|
-
console.log('Installing esbuild...')
|
|
33
|
-
try {
|
|
34
|
-
execSync('npm install esbuild --save-dev', { cwd: ROOT, stdio: 'inherit' })
|
|
35
|
-
return true
|
|
36
|
-
} catch (error) {
|
|
37
|
-
console.error('Failed to install esbuild:', error.message)
|
|
38
|
-
return false
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Clean and create dist directory
|
|
45
|
-
*/
|
|
46
|
-
function clean() {
|
|
47
|
-
if (fs.existsSync(DIST)) {
|
|
48
|
-
fs.rmSync(DIST, { recursive: true })
|
|
49
|
-
}
|
|
50
|
-
fs.mkdirSync(DIST, { recursive: true })
|
|
51
|
-
fs.mkdirSync(path.join(DIST, 'bin'), { recursive: true })
|
|
52
|
-
fs.mkdirSync(path.join(DIST, 'core', 'infrastructure'), { recursive: true })
|
|
53
|
-
fs.mkdirSync(path.join(DIST, 'core', 'utils'), { recursive: true })
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Build all modules
|
|
58
|
-
*/
|
|
59
|
-
async function build() {
|
|
60
|
-
const esbuild = require('esbuild')
|
|
61
|
-
|
|
62
|
-
console.log('Building for Node.js...\n')
|
|
63
|
-
|
|
64
|
-
// 1. CLI entry point (ESM)
|
|
65
|
-
console.log(' → bin/prjct.mjs')
|
|
66
|
-
await esbuild.build({
|
|
67
|
-
entryPoints: [path.join(ROOT, 'bin/prjct.ts')],
|
|
68
|
-
outfile: path.join(DIST, 'bin', 'prjct.mjs'),
|
|
69
|
-
bundle: true,
|
|
70
|
-
platform: 'node',
|
|
71
|
-
target: 'node18',
|
|
72
|
-
format: 'esm',
|
|
73
|
-
sourcemap: false,
|
|
74
|
-
minify: false,
|
|
75
|
-
keepNames: true,
|
|
76
|
-
packages: 'external',
|
|
77
|
-
banner: {
|
|
78
|
-
js: `#!/usr/bin/env node
|
|
79
|
-
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
80
|
-
import { dirname as __pathDirname } from 'path';
|
|
81
|
-
const __filename = __fileURLToPath(import.meta.url);
|
|
82
|
-
const __dirname = __pathDirname(__filename);`,
|
|
83
|
-
},
|
|
84
|
-
})
|
|
85
|
-
fs.chmodSync(path.join(DIST, 'bin', 'prjct.mjs'), 0o755)
|
|
86
|
-
|
|
87
|
-
// 2. Setup module (CJS - for postinstall)
|
|
88
|
-
console.log(' → core/infrastructure/setup.js')
|
|
89
|
-
await esbuild.build({
|
|
90
|
-
entryPoints: [path.join(ROOT, 'core/infrastructure/setup.ts')],
|
|
91
|
-
outfile: path.join(DIST, 'core', 'infrastructure', 'setup.js'),
|
|
92
|
-
bundle: true,
|
|
93
|
-
platform: 'node',
|
|
94
|
-
target: 'node18',
|
|
95
|
-
format: 'cjs',
|
|
96
|
-
sourcemap: false,
|
|
97
|
-
minify: false,
|
|
98
|
-
keepNames: true,
|
|
99
|
-
packages: 'external',
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
// 3. Command installer (CJS)
|
|
103
|
-
console.log(' → core/infrastructure/command-installer.js')
|
|
104
|
-
await esbuild.build({
|
|
105
|
-
entryPoints: [path.join(ROOT, 'core/infrastructure/command-installer.ts')],
|
|
106
|
-
outfile: path.join(DIST, 'core', 'infrastructure', 'command-installer.js'),
|
|
107
|
-
bundle: true,
|
|
108
|
-
platform: 'node',
|
|
109
|
-
target: 'node18',
|
|
110
|
-
format: 'cjs',
|
|
111
|
-
sourcemap: false,
|
|
112
|
-
minify: false,
|
|
113
|
-
keepNames: true,
|
|
114
|
-
packages: 'external',
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
// 4. Editors config (CJS)
|
|
118
|
-
console.log(' → core/infrastructure/editors-config.js')
|
|
119
|
-
await esbuild.build({
|
|
120
|
-
entryPoints: [path.join(ROOT, 'core/infrastructure/editors-config.ts')],
|
|
121
|
-
outfile: path.join(DIST, 'core', 'infrastructure', 'editors-config.js'),
|
|
122
|
-
bundle: true,
|
|
123
|
-
platform: 'node',
|
|
124
|
-
target: 'node18',
|
|
125
|
-
format: 'cjs',
|
|
126
|
-
sourcemap: false,
|
|
127
|
-
minify: false,
|
|
128
|
-
keepNames: true,
|
|
129
|
-
packages: 'external',
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
// 5. Version util (CJS)
|
|
133
|
-
console.log(' → core/utils/version.js')
|
|
134
|
-
await esbuild.build({
|
|
135
|
-
entryPoints: [path.join(ROOT, 'core/utils/version.ts')],
|
|
136
|
-
outfile: path.join(DIST, 'core', 'utils', 'version.js'),
|
|
137
|
-
bundle: true,
|
|
138
|
-
platform: 'node',
|
|
139
|
-
target: 'node18',
|
|
140
|
-
format: 'cjs',
|
|
141
|
-
sourcemap: false,
|
|
142
|
-
minify: false,
|
|
143
|
-
keepNames: true,
|
|
144
|
-
packages: 'external',
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
console.log('\nBuild complete!')
|
|
148
|
-
console.log(`Output: ${DIST}/`)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Main
|
|
153
|
-
*/
|
|
154
|
-
async function main() {
|
|
155
|
-
console.log('prjct-cli build script')
|
|
156
|
-
console.log('======================\n')
|
|
157
|
-
|
|
158
|
-
if (!ensureEsbuild()) {
|
|
159
|
-
process.exit(1)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
clean()
|
|
163
|
-
await build()
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
main().catch((error) => {
|
|
167
|
-
console.error('Build failed:', error)
|
|
168
|
-
process.exit(1)
|
|
169
|
-
})
|