prjct-cli 0.50.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/core/agentic/memory-system.ts +96 -18
- package/core/commands/analysis.ts +199 -83
- package/core/services/diff-generator.ts +356 -0
- package/core/services/sync-service.ts +6 -0
- package/core/types/memory.ts +41 -2
- package/dist/bin/prjct.mjs +694 -333
- package/package.json +1 -1
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff Generator for Sync Preview
|
|
3
|
+
*
|
|
4
|
+
* Generates human-readable diffs between old and new context files.
|
|
5
|
+
* Shows what will change before sync applies.
|
|
6
|
+
*
|
|
7
|
+
* @see PRJ-125
|
|
8
|
+
* @module services/diff-generator
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import chalk from 'chalk'
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
export interface DiffSection {
|
|
18
|
+
name: string
|
|
19
|
+
type: 'added' | 'modified' | 'removed' | 'unchanged'
|
|
20
|
+
before?: string
|
|
21
|
+
after?: string
|
|
22
|
+
lineCount: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PreservedInfo {
|
|
26
|
+
name: string
|
|
27
|
+
lineCount: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SyncDiff {
|
|
31
|
+
hasChanges: boolean
|
|
32
|
+
added: DiffSection[]
|
|
33
|
+
modified: DiffSection[]
|
|
34
|
+
removed: DiffSection[]
|
|
35
|
+
preserved: PreservedInfo[]
|
|
36
|
+
tokensBefore: number
|
|
37
|
+
tokensAfter: number
|
|
38
|
+
tokenDelta: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DiffOptions {
|
|
42
|
+
showFullDiff?: boolean
|
|
43
|
+
colorize?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// Constants
|
|
48
|
+
// =============================================================================
|
|
49
|
+
|
|
50
|
+
const CHARS_PER_TOKEN = 4
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Token Estimation
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Estimate token count for content
|
|
58
|
+
*/
|
|
59
|
+
export function estimateTokens(content: string): number {
|
|
60
|
+
return Math.ceil(content.length / CHARS_PER_TOKEN)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// Section Parsing
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
interface ParsedSection {
|
|
68
|
+
name: string
|
|
69
|
+
content: string
|
|
70
|
+
startLine: number
|
|
71
|
+
endLine: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse markdown into sections based on headers
|
|
76
|
+
*/
|
|
77
|
+
export function parseMarkdownSections(content: string): ParsedSection[] {
|
|
78
|
+
const lines = content.split('\n')
|
|
79
|
+
const sections: ParsedSection[] = []
|
|
80
|
+
let currentSection: ParsedSection | null = null
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < lines.length; i++) {
|
|
83
|
+
const line = lines[i]
|
|
84
|
+
const headerMatch = line.match(/^(#{1,3})\s+(.+)$/)
|
|
85
|
+
|
|
86
|
+
if (headerMatch) {
|
|
87
|
+
// Save previous section
|
|
88
|
+
if (currentSection) {
|
|
89
|
+
currentSection.endLine = i - 1
|
|
90
|
+
sections.push(currentSection)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Start new section
|
|
94
|
+
currentSection = {
|
|
95
|
+
name: headerMatch[2].trim(),
|
|
96
|
+
content: line,
|
|
97
|
+
startLine: i,
|
|
98
|
+
endLine: i,
|
|
99
|
+
}
|
|
100
|
+
} else if (currentSection) {
|
|
101
|
+
currentSection.content += `\n${line}`
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Save last section
|
|
106
|
+
if (currentSection) {
|
|
107
|
+
currentSection.endLine = lines.length - 1
|
|
108
|
+
sections.push(currentSection)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return sections
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if content is within a preserve block
|
|
116
|
+
*/
|
|
117
|
+
function isPreservedSection(content: string): boolean {
|
|
118
|
+
return content.includes('<!-- prjct:preserve')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// =============================================================================
|
|
122
|
+
// Diff Generation
|
|
123
|
+
// =============================================================================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generate diff between old and new content
|
|
127
|
+
*/
|
|
128
|
+
export function generateSyncDiff(oldContent: string, newContent: string): SyncDiff {
|
|
129
|
+
const oldSections = parseMarkdownSections(oldContent)
|
|
130
|
+
const newSections = parseMarkdownSections(newContent)
|
|
131
|
+
|
|
132
|
+
const diff: SyncDiff = {
|
|
133
|
+
hasChanges: false,
|
|
134
|
+
added: [],
|
|
135
|
+
modified: [],
|
|
136
|
+
removed: [],
|
|
137
|
+
preserved: [],
|
|
138
|
+
tokensBefore: estimateTokens(oldContent),
|
|
139
|
+
tokensAfter: estimateTokens(newContent),
|
|
140
|
+
tokenDelta: 0,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
diff.tokenDelta = diff.tokensAfter - diff.tokensBefore
|
|
144
|
+
|
|
145
|
+
// Create maps for quick lookup
|
|
146
|
+
const oldMap = new Map(oldSections.map((s) => [s.name.toLowerCase(), s]))
|
|
147
|
+
const newMap = new Map(newSections.map((s) => [s.name.toLowerCase(), s]))
|
|
148
|
+
|
|
149
|
+
// Find preserved sections in old content
|
|
150
|
+
for (const section of oldSections) {
|
|
151
|
+
if (isPreservedSection(section.content)) {
|
|
152
|
+
diff.preserved.push({
|
|
153
|
+
name: section.name,
|
|
154
|
+
lineCount: section.content.split('\n').length,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Find added and modified sections
|
|
160
|
+
for (const newSection of newSections) {
|
|
161
|
+
const key = newSection.name.toLowerCase()
|
|
162
|
+
const oldSection = oldMap.get(key)
|
|
163
|
+
|
|
164
|
+
if (!oldSection) {
|
|
165
|
+
// New section
|
|
166
|
+
diff.added.push({
|
|
167
|
+
name: newSection.name,
|
|
168
|
+
type: 'added',
|
|
169
|
+
after: newSection.content,
|
|
170
|
+
lineCount: newSection.content.split('\n').length,
|
|
171
|
+
})
|
|
172
|
+
diff.hasChanges = true
|
|
173
|
+
} else if (oldSection.content.trim() !== newSection.content.trim()) {
|
|
174
|
+
// Modified section (skip if preserved)
|
|
175
|
+
if (!isPreservedSection(oldSection.content)) {
|
|
176
|
+
diff.modified.push({
|
|
177
|
+
name: newSection.name,
|
|
178
|
+
type: 'modified',
|
|
179
|
+
before: oldSection.content,
|
|
180
|
+
after: newSection.content,
|
|
181
|
+
lineCount: newSection.content.split('\n').length,
|
|
182
|
+
})
|
|
183
|
+
diff.hasChanges = true
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Find removed sections
|
|
189
|
+
for (const oldSection of oldSections) {
|
|
190
|
+
const key = oldSection.name.toLowerCase()
|
|
191
|
+
if (!newMap.has(key) && !isPreservedSection(oldSection.content)) {
|
|
192
|
+
diff.removed.push({
|
|
193
|
+
name: oldSection.name,
|
|
194
|
+
type: 'removed',
|
|
195
|
+
before: oldSection.content,
|
|
196
|
+
lineCount: oldSection.content.split('\n').length,
|
|
197
|
+
})
|
|
198
|
+
diff.hasChanges = true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return diff
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// =============================================================================
|
|
206
|
+
// Diff Formatting
|
|
207
|
+
// =============================================================================
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Format diff for terminal display
|
|
211
|
+
*/
|
|
212
|
+
export function formatDiffPreview(diff: SyncDiff, options: DiffOptions = {}): string {
|
|
213
|
+
const { colorize = true } = options
|
|
214
|
+
const lines: string[] = []
|
|
215
|
+
|
|
216
|
+
const green = colorize ? chalk.green : (s: string) => s
|
|
217
|
+
const red = colorize ? chalk.red : (s: string) => s
|
|
218
|
+
const yellow = colorize ? chalk.yellow : (s: string) => s
|
|
219
|
+
const dim = colorize ? chalk.dim : (s: string) => s
|
|
220
|
+
const bold = colorize ? chalk.bold : (s: string) => s
|
|
221
|
+
|
|
222
|
+
if (!diff.hasChanges) {
|
|
223
|
+
lines.push(dim('No changes detected (context is up to date)'))
|
|
224
|
+
return lines.join('\n')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
lines.push('')
|
|
228
|
+
lines.push(bold('📋 Changes to context files:'))
|
|
229
|
+
lines.push('')
|
|
230
|
+
|
|
231
|
+
// Added sections
|
|
232
|
+
if (diff.added.length > 0) {
|
|
233
|
+
for (const section of diff.added) {
|
|
234
|
+
lines.push(green(`+ │ + ${section.name} (new)`))
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Modified sections
|
|
239
|
+
if (diff.modified.length > 0) {
|
|
240
|
+
for (const section of diff.modified) {
|
|
241
|
+
lines.push(yellow(`~ │ ${section.name} (modified)`))
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Removed sections
|
|
246
|
+
if (diff.removed.length > 0) {
|
|
247
|
+
for (const section of diff.removed) {
|
|
248
|
+
lines.push(red(`- │ - ${section.name} (removed)`))
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Preserved sections
|
|
253
|
+
if (diff.preserved.length > 0) {
|
|
254
|
+
lines.push('')
|
|
255
|
+
lines.push(dim(' ## Your Customizations'))
|
|
256
|
+
for (const section of diff.preserved) {
|
|
257
|
+
lines.push(dim(` │ ✓ ${section.name} (${section.lineCount} lines preserved)`))
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Summary
|
|
262
|
+
lines.push('')
|
|
263
|
+
lines.push(dim('────────────────────────────────'))
|
|
264
|
+
|
|
265
|
+
const summaryParts: string[] = []
|
|
266
|
+
if (diff.added.length > 0) summaryParts.push(green(`+${diff.added.length} added`))
|
|
267
|
+
if (diff.modified.length > 0) summaryParts.push(yellow(`~${diff.modified.length} modified`))
|
|
268
|
+
if (diff.removed.length > 0) summaryParts.push(red(`-${diff.removed.length} removed`))
|
|
269
|
+
|
|
270
|
+
lines.push(`Summary: ${summaryParts.join(', ') || 'no changes'}`)
|
|
271
|
+
|
|
272
|
+
// Token delta
|
|
273
|
+
const tokenSign = diff.tokenDelta >= 0 ? '+' : ''
|
|
274
|
+
const tokenColor = diff.tokenDelta >= 0 ? green : red
|
|
275
|
+
lines.push(
|
|
276
|
+
`Tokens: ${diff.tokensBefore.toLocaleString()} → ${diff.tokensAfter.toLocaleString()} (${tokenColor(tokenSign + diff.tokenDelta.toLocaleString())})`
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
lines.push('')
|
|
280
|
+
|
|
281
|
+
return lines.join('\n')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Format full git-style diff
|
|
286
|
+
*/
|
|
287
|
+
export function formatFullDiff(diff: SyncDiff, options: DiffOptions = {}): string {
|
|
288
|
+
const { colorize = true } = options
|
|
289
|
+
const lines: string[] = []
|
|
290
|
+
|
|
291
|
+
const green = colorize ? chalk.green : (s: string) => s
|
|
292
|
+
const red = colorize ? chalk.red : (s: string) => s
|
|
293
|
+
const cyan = colorize ? chalk.cyan : (s: string) => s
|
|
294
|
+
const dim = colorize ? chalk.dim : (s: string) => s
|
|
295
|
+
|
|
296
|
+
// Added sections
|
|
297
|
+
for (const section of diff.added) {
|
|
298
|
+
lines.push(cyan(`@@ +${section.name} @@`))
|
|
299
|
+
if (section.after) {
|
|
300
|
+
for (const line of section.after.split('\n')) {
|
|
301
|
+
lines.push(green(`+ ${line}`))
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
lines.push('')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Modified sections
|
|
308
|
+
for (const section of diff.modified) {
|
|
309
|
+
lines.push(cyan(`@@ ${section.name} @@`))
|
|
310
|
+
if (section.before) {
|
|
311
|
+
for (const line of section.before.split('\n').slice(0, 5)) {
|
|
312
|
+
lines.push(red(`- ${line}`))
|
|
313
|
+
}
|
|
314
|
+
if (section.before.split('\n').length > 5) {
|
|
315
|
+
lines.push(dim(` ... ${section.before.split('\n').length - 5} more lines`))
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (section.after) {
|
|
319
|
+
for (const line of section.after.split('\n').slice(0, 5)) {
|
|
320
|
+
lines.push(green(`+ ${line}`))
|
|
321
|
+
}
|
|
322
|
+
if (section.after.split('\n').length > 5) {
|
|
323
|
+
lines.push(dim(` ... ${section.after.split('\n').length - 5} more lines`))
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
lines.push('')
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Removed sections
|
|
330
|
+
for (const section of diff.removed) {
|
|
331
|
+
lines.push(cyan(`@@ -${section.name} @@`))
|
|
332
|
+
if (section.before) {
|
|
333
|
+
for (const line of section.before.split('\n').slice(0, 5)) {
|
|
334
|
+
lines.push(red(`- ${line}`))
|
|
335
|
+
}
|
|
336
|
+
if (section.before.split('\n').length > 5) {
|
|
337
|
+
lines.push(dim(` ... ${section.before.split('\n').length - 5} more lines`))
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
lines.push('')
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return lines.join('\n')
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// =============================================================================
|
|
347
|
+
// Exports
|
|
348
|
+
// =============================================================================
|
|
349
|
+
|
|
350
|
+
export default {
|
|
351
|
+
generateSyncDiff,
|
|
352
|
+
formatDiffPreview,
|
|
353
|
+
formatFullDiff,
|
|
354
|
+
estimateTokens,
|
|
355
|
+
parseMarkdownSections,
|
|
356
|
+
}
|
|
@@ -31,6 +31,7 @@ import pathManager from '../infrastructure/path-manager'
|
|
|
31
31
|
import { metricsStorage } from '../storage/metrics-storage'
|
|
32
32
|
import dateHelper from '../utils/date-helper'
|
|
33
33
|
import { ContextFileGenerator } from './context-generator'
|
|
34
|
+
import type { SyncDiff } from './diff-generator'
|
|
34
35
|
import { type StackDetection, StackDetector } from './stack-detector'
|
|
35
36
|
|
|
36
37
|
const execAsync = promisify(exec)
|
|
@@ -104,10 +105,15 @@ interface SyncResult {
|
|
|
104
105
|
aiTools: AIToolResult[]
|
|
105
106
|
syncMetrics?: SyncMetrics
|
|
106
107
|
error?: string
|
|
108
|
+
// Preview mode fields
|
|
109
|
+
isPreview?: boolean
|
|
110
|
+
previewDiff?: SyncDiff
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
interface SyncOptions {
|
|
110
114
|
aiTools?: string[] // AI tools to generate context for (default: claude, cursor)
|
|
115
|
+
preview?: boolean // If true, return diff without applying changes
|
|
116
|
+
skipConfirmation?: boolean // If true, apply changes without confirmation (--yes flag)
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
// ============================================================================
|
package/core/types/memory.ts
CHANGED
|
@@ -43,6 +43,10 @@ export interface Memory {
|
|
|
43
43
|
userTriggered: boolean
|
|
44
44
|
createdAt: string
|
|
45
45
|
updatedAt: string
|
|
46
|
+
/** Confidence level for this memory (optional for backward compatibility) */
|
|
47
|
+
confidence?: ConfidenceLevel
|
|
48
|
+
/** Number of times this memory was reinforced */
|
|
49
|
+
observationCount?: number
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/**
|
|
@@ -123,8 +127,10 @@ export interface Decision {
|
|
|
123
127
|
count: number
|
|
124
128
|
firstSeen: string
|
|
125
129
|
lastSeen: string
|
|
126
|
-
confidence:
|
|
130
|
+
confidence: ConfidenceLevel
|
|
127
131
|
contexts: string[]
|
|
132
|
+
/** Whether user explicitly confirmed this decision */
|
|
133
|
+
userConfirmed?: boolean
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
/**
|
|
@@ -141,14 +147,47 @@ export interface Workflow {
|
|
|
141
147
|
successRate?: number
|
|
142
148
|
/** Steps in the workflow */
|
|
143
149
|
steps?: string[]
|
|
150
|
+
/** Confidence level based on execution count */
|
|
151
|
+
confidence?: ConfidenceLevel
|
|
152
|
+
/** Whether user explicitly confirmed this workflow */
|
|
153
|
+
userConfirmed?: boolean
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
/**
|
|
147
|
-
*
|
|
157
|
+
* Confidence level for stored preferences and decisions.
|
|
158
|
+
* @see PRJ-104
|
|
159
|
+
*/
|
|
160
|
+
export type ConfidenceLevel = 'low' | 'medium' | 'high'
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Calculate confidence level from observation count.
|
|
164
|
+
* - low: 1-2 observations
|
|
165
|
+
* - medium: 3-5 observations
|
|
166
|
+
* - high: 6+ observations or explicit user confirmation
|
|
167
|
+
*/
|
|
168
|
+
export function calculateConfidence(
|
|
169
|
+
count: number,
|
|
170
|
+
userConfirmed: boolean = false
|
|
171
|
+
): ConfidenceLevel {
|
|
172
|
+
if (userConfirmed) return 'high'
|
|
173
|
+
if (count >= 6) return 'high'
|
|
174
|
+
if (count >= 3) return 'medium'
|
|
175
|
+
return 'low'
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A user preference value with confidence scoring.
|
|
180
|
+
* @see PRJ-104
|
|
148
181
|
*/
|
|
149
182
|
export interface Preference {
|
|
150
183
|
value: string | number | boolean
|
|
151
184
|
updatedAt: string
|
|
185
|
+
/** Confidence level based on observations */
|
|
186
|
+
confidence: ConfidenceLevel
|
|
187
|
+
/** Number of times this preference was observed */
|
|
188
|
+
observationCount: number
|
|
189
|
+
/** Whether user explicitly confirmed this preference */
|
|
190
|
+
userConfirmed: boolean
|
|
152
191
|
}
|
|
153
192
|
|
|
154
193
|
/**
|