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 CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.52.0] - 2026-01-30
4
+
5
+ ### Features
6
+
7
+ - Add confidence scores to all stored preferences - PRJ-104 (#78)
8
+
9
+
10
+ ## [0.52.0] - 2026-01-30
11
+
12
+ ### Added
13
+
14
+ - **Confidence scores for stored preferences** (PRJ-104)
15
+ - All preferences, decisions, and workflows now track confidence level
16
+ - Confidence: `low` (1-2 obs), `medium` (3-5 obs), `high` (6+ or confirmed)
17
+ - Added `confirmPreference()`, `confirmDecision()`, `confirmWorkflow()` methods
18
+ - User confirmation immediately sets confidence to `high`
19
+ - Added `calculateConfidence()` utility function
20
+
21
+
22
+ ## [0.51.0] - 2026-01-30
23
+
24
+ ### Features
25
+
26
+ - Context diff preview before sync applies - PRJ-125 (#77)
27
+
28
+
29
+ ## [0.51.0] - 2026-01-30
30
+
31
+ ### Added
32
+
33
+ - **Context Diff Preview** (PRJ-125)
34
+ - See what changes sync will make before they're applied
35
+ - Interactive confirmation: apply, cancel, or show full diff
36
+ - Preserved sections clearly marked in preview
37
+ - Token count delta displayed
38
+ - `--preview` flag for dry-run only
39
+ - `--yes` flag to skip confirmation
40
+
41
+
3
42
  ## [0.50.0] - 2026-01-30
4
43
 
5
44
  ### Features
@@ -22,6 +22,7 @@ import { appendJsonLine, getLastJsonLines } from '../utils/jsonl-helper'
22
22
 
23
23
  // Re-export types from canonical location
24
24
  export type {
25
+ ConfidenceLevel,
25
26
  Decision,
26
27
  HistoryEntry,
27
28
  HistoryEventType,
@@ -35,7 +36,7 @@ export type {
35
36
  Workflow,
36
37
  } from '../types/memory'
37
38
 
38
- export { MEMORY_TAGS } from '../types/memory'
39
+ export { calculateConfidence, MEMORY_TAGS } from '../types/memory'
39
40
 
40
41
  import type {
41
42
  HistoryEntry,
@@ -49,7 +50,7 @@ import type {
49
50
  Workflow,
50
51
  } from '../types/memory'
51
52
 
52
- import { MEMORY_TAGS } from '../types/memory'
53
+ import { calculateConfidence, MEMORY_TAGS } from '../types/memory'
53
54
 
54
55
  // =============================================================================
55
56
  // Base Store
@@ -306,7 +307,8 @@ export class PatternStore extends CachedStore<Patterns> {
306
307
  projectId: string,
307
308
  key: string,
308
309
  value: string,
309
- context: string = ''
310
+ context: string = '',
311
+ options: { userConfirmed?: boolean } = {}
310
312
  ): Promise<void> {
311
313
  const patterns = await this.load(projectId)
312
314
  const now = getTimestamp()
@@ -317,11 +319,14 @@ export class PatternStore extends CachedStore<Patterns> {
317
319
  count: 1,
318
320
  firstSeen: now,
319
321
  lastSeen: now,
320
- confidence: 'low',
322
+ confidence: options.userConfirmed ? 'high' : 'low',
321
323
  contexts: [context].filter(Boolean),
322
- }
324
+ userConfirmed: options.userConfirmed || false,
325
+ } as Patterns['decisions'][string]
323
326
  } else {
324
- const decision = patterns.decisions[key]
327
+ const decision = patterns.decisions[key] as Patterns['decisions'][string] & {
328
+ userConfirmed?: boolean
329
+ }
325
330
 
326
331
  if (decision.value === value) {
327
332
  decision.count++
@@ -329,23 +334,36 @@ export class PatternStore extends CachedStore<Patterns> {
329
334
  if (context && !decision.contexts.includes(context)) {
330
335
  decision.contexts.push(context)
331
336
  }
332
-
333
- if (decision.count >= 5) {
334
- decision.confidence = 'high'
335
- } else if (decision.count >= 3) {
336
- decision.confidence = 'medium'
337
+ if (options.userConfirmed) {
338
+ decision.userConfirmed = true
337
339
  }
340
+ decision.confidence = calculateConfidence(decision.count, decision.userConfirmed)
338
341
  } else {
339
342
  decision.value = value
340
343
  decision.count = 1
341
344
  decision.lastSeen = now
342
- decision.confidence = 'low'
345
+ decision.userConfirmed = options.userConfirmed || false
346
+ decision.confidence = options.userConfirmed ? 'high' : 'low'
343
347
  }
344
348
  }
345
349
 
346
350
  await this.save(projectId)
347
351
  }
348
352
 
353
+ async confirmDecision(projectId: string, key: string): Promise<boolean> {
354
+ const patterns = await this.load(projectId)
355
+ const decision = patterns.decisions[key] as
356
+ | (Patterns['decisions'][string] & { userConfirmed?: boolean })
357
+ | undefined
358
+ if (!decision) return false
359
+
360
+ decision.userConfirmed = true
361
+ decision.confidence = 'high'
362
+ decision.lastSeen = getTimestamp()
363
+ await this.save(projectId)
364
+ return true
365
+ }
366
+
349
367
  async getDecision(
350
368
  projectId: string,
351
369
  key: string
@@ -378,15 +396,31 @@ export class PatternStore extends CachedStore<Patterns> {
378
396
  count: 1,
379
397
  firstSeen: now,
380
398
  lastSeen: now,
399
+ confidence: 'low',
400
+ userConfirmed: false,
381
401
  }
382
402
  } else {
383
- patterns.workflows[workflowName].count++
384
- patterns.workflows[workflowName].lastSeen = now
403
+ const workflow = patterns.workflows[workflowName]
404
+ workflow.count++
405
+ workflow.lastSeen = now
406
+ workflow.confidence = calculateConfidence(workflow.count, workflow.userConfirmed)
385
407
  }
386
408
 
387
409
  await this.save(projectId)
388
410
  }
389
411
 
412
+ async confirmWorkflow(projectId: string, workflowName: string): Promise<boolean> {
413
+ const patterns = await this.load(projectId)
414
+ const workflow = patterns.workflows[workflowName]
415
+ if (!workflow) return false
416
+
417
+ workflow.userConfirmed = true
418
+ workflow.confidence = 'high'
419
+ workflow.lastSeen = getTimestamp()
420
+ await this.save(projectId)
421
+ return true
422
+ }
423
+
390
424
  async getWorkflow(projectId: string, workflowName: string): Promise<Workflow | null> {
391
425
  const patterns = await this.load(projectId)
392
426
  const workflow = patterns.workflows[workflowName]
@@ -395,12 +429,39 @@ export class PatternStore extends CachedStore<Patterns> {
395
429
  return workflow
396
430
  }
397
431
 
398
- async setPreference(projectId: string, key: string, value: Preference['value']): Promise<void> {
432
+ async setPreference(
433
+ projectId: string,
434
+ key: string,
435
+ value: Preference['value'],
436
+ options: { userConfirmed?: boolean } = {}
437
+ ): Promise<void> {
399
438
  const patterns = await this.load(projectId)
400
- patterns.preferences[key] = { value, updatedAt: getTimestamp() }
439
+ const existing = patterns.preferences[key]
440
+ const observationCount = existing ? existing.observationCount + 1 : 1
441
+ const userConfirmed = options.userConfirmed || existing?.userConfirmed || false
442
+
443
+ patterns.preferences[key] = {
444
+ value,
445
+ updatedAt: getTimestamp(),
446
+ confidence: calculateConfidence(observationCount, userConfirmed),
447
+ observationCount,
448
+ userConfirmed,
449
+ }
401
450
  await this.save(projectId)
402
451
  }
403
452
 
453
+ async confirmPreference(projectId: string, key: string): Promise<boolean> {
454
+ const patterns = await this.load(projectId)
455
+ const pref = patterns.preferences[key]
456
+ if (!pref) return false
457
+
458
+ pref.userConfirmed = true
459
+ pref.confidence = 'high'
460
+ pref.updatedAt = getTimestamp()
461
+ await this.save(projectId)
462
+ return true
463
+ }
464
+
404
465
  async getPreference(
405
466
  projectId: string,
406
467
  key: string,
@@ -857,14 +918,31 @@ export class MemorySystem {
857
918
  return this._patternStore.getWorkflow(projectId, workflowName)
858
919
  }
859
920
 
860
- setPreference(projectId: string, key: string, value: Preference['value']): Promise<void> {
861
- return this._patternStore.setPreference(projectId, key, value)
921
+ setPreference(
922
+ projectId: string,
923
+ key: string,
924
+ value: Preference['value'],
925
+ options?: { userConfirmed?: boolean }
926
+ ): Promise<void> {
927
+ return this._patternStore.setPreference(projectId, key, value, options)
862
928
  }
863
929
 
864
930
  getPreference(projectId: string, key: string, defaultValue?: unknown): Promise<unknown> {
865
931
  return this._patternStore.getPreference(projectId, key, defaultValue)
866
932
  }
867
933
 
934
+ confirmPreference(projectId: string, key: string): Promise<boolean> {
935
+ return this._patternStore.confirmPreference(projectId, key)
936
+ }
937
+
938
+ confirmDecision(projectId: string, key: string): Promise<boolean> {
939
+ return this._patternStore.confirmDecision(projectId, key)
940
+ }
941
+
942
+ confirmWorkflow(projectId: string, workflowName: string): Promise<boolean> {
943
+ return this._patternStore.confirmWorkflow(projectId, workflowName)
944
+ }
945
+
868
946
  getPatternsSummary(projectId: string) {
869
947
  return this._patternStore.getPatternsSummary(projectId)
870
948
  }
@@ -2,15 +2,19 @@
2
2
  * Analysis Commands: analyze, sync, and related helpers
3
3
  */
4
4
 
5
+ import fs from 'node:fs/promises'
5
6
  import path from 'node:path'
7
+ import prompts from 'prompts'
6
8
  import { generateContext } from '../context/generator'
7
9
  import analyzer from '../domain/analyzer'
8
10
  import commandInstaller from '../infrastructure/command-installer'
9
11
  import { formatCost } from '../schemas/metrics'
10
12
  import { syncService } from '../services'
13
+ import { formatDiffPreview, formatFullDiff, generateSyncDiff } from '../services/diff-generator'
11
14
  import { metricsStorage } from '../storage/metrics-storage'
12
15
  import type { AnalyzeOptions, CommandResult, ProjectContext } from '../types'
13
16
  import { showNextSteps } from '../utils/next-steps'
17
+ import out from '../utils/output'
14
18
  import {
15
19
  configManager,
16
20
  contextBuilder,
@@ -195,7 +199,7 @@ export class AnalysisCommands extends PrjctCommandsBase {
195
199
  }
196
200
 
197
201
  /**
198
- * /p:sync - Comprehensive project sync
202
+ * /p:sync - Comprehensive project sync with diff preview
199
203
  *
200
204
  * Uses syncService to do ALL operations in one TypeScript execution:
201
205
  * - Git analysis
@@ -205,120 +209,232 @@ export class AnalysisCommands extends PrjctCommandsBase {
205
209
  * - Skill configuration
206
210
  * - State updates
207
211
  *
212
+ * Options:
213
+ * - --preview: Show what would change without applying
214
+ * - --yes: Skip confirmation prompt
215
+ *
208
216
  * This eliminates the need for Claude to make 50+ individual tool calls.
217
+ *
218
+ * @see PRJ-125
209
219
  */
210
220
  async sync(
211
221
  projectPath: string = process.cwd(),
212
- options: { aiTools?: string[] } = {}
222
+ options: { aiTools?: string[]; preview?: boolean; yes?: boolean } = {}
213
223
  ): Promise<CommandResult> {
214
224
  try {
215
225
  const initResult = await this.ensureProjectInit(projectPath)
216
226
  if (!initResult.success) return initResult
217
227
 
218
- const startTime = Date.now()
219
- console.log('🔄 Syncing project...\n')
228
+ const projectId = await configManager.getProjectId(projectPath)
229
+ if (!projectId) {
230
+ out.failWithHint('NO_PROJECT_ID')
231
+ return { success: false, error: 'No project ID found' }
232
+ }
220
233
 
221
- // Use syncService to do EVERYTHING in one call
222
- const result = await syncService.sync(projectPath, { aiTools: options.aiTools })
234
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
235
+ const startTime = Date.now()
223
236
 
224
- if (!result.success) {
225
- console.error('❌ Sync failed:', result.error)
226
- return { success: false, error: result.error }
237
+ // Generate diff preview if we have existing context
238
+ const claudeMdPath = path.join(globalPath, 'context', 'CLAUDE.md')
239
+ let existingContent: string | null = null
240
+ try {
241
+ existingContent = await fs.readFile(claudeMdPath, 'utf-8')
242
+ } catch {
243
+ // No existing file - first sync
227
244
  }
228
245
 
229
- // Update global config
230
- const globalConfigResult = await commandInstaller.installGlobalConfig()
231
- if (globalConfigResult.success) {
232
- console.log(`📝 Updated ${pathManager.getDisplayPath(globalConfigResult.path!)}`)
233
- }
246
+ // For preview mode or when we have existing content, show diff first
247
+ if (existingContent && !options.yes) {
248
+ out.spin('Analyzing changes...')
234
249
 
235
- // Format output
236
- console.log(`🔄 Project synced to prjct v${result.cliVersion}\n`)
250
+ // Do a dry-run sync to see what would change
251
+ const result = await syncService.sync(projectPath, { aiTools: options.aiTools })
237
252
 
238
- console.log('📊 Project Stats')
239
- console.log(`├── Files: ~${result.stats.fileCount}`)
240
- console.log(`├── Commits: ${result.git.commits}`)
241
- console.log(`├── Version: ${result.stats.version}`)
242
- console.log(`└── Stack: ${result.stats.ecosystem}\n`)
253
+ if (!result.success) {
254
+ out.fail(result.error || 'Sync failed')
255
+ return { success: false, error: result.error }
256
+ }
243
257
 
244
- console.log('🌿 Git Status')
245
- console.log(`├── Branch: ${result.git.branch}`)
246
- console.log(`├── Uncommitted: ${result.git.hasChanges ? 'Yes' : 'Clean'}`)
247
- console.log(`└── Recent: ${result.git.weeklyCommits} commits this week\n`)
258
+ // Read the newly generated CLAUDE.md
259
+ let newContent: string
260
+ try {
261
+ newContent = await fs.readFile(claudeMdPath, 'utf-8')
262
+ } catch {
263
+ newContent = ''
264
+ }
248
265
 
249
- console.log('📁 Context Updated')
250
- for (const file of result.contextFiles) {
251
- console.log(`├── ${file}`)
252
- }
253
- console.log('')
266
+ // Generate diff
267
+ const diff = generateSyncDiff(existingContent, newContent)
268
+
269
+ out.stop()
254
270
 
255
- // Show AI Tools generated (multi-agent output)
256
- if (result.aiTools && result.aiTools.length > 0) {
257
- const successTools = result.aiTools.filter((t) => t.success)
258
- console.log(`🤖 AI Tools Context (${successTools.length})`)
259
- for (const tool of result.aiTools) {
260
- const status = tool.success ? '✓' : '✗'
261
- console.log(`├── ${status} ${tool.outputFile} (${tool.toolId})`)
271
+ if (!diff.hasChanges) {
272
+ out.done('No changes detected (context is up to date)')
273
+ return { success: true, message: 'No changes' }
262
274
  }
263
- console.log('')
264
- }
265
275
 
266
- const workflowAgents = result.agents.filter((a) => a.type === 'workflow').map((a) => a.name)
267
- const domainAgents = result.agents.filter((a) => a.type === 'domain').map((a) => a.name)
276
+ // Show diff preview
277
+ console.log(formatDiffPreview(diff))
278
+
279
+ // Preview-only mode - don't apply
280
+ if (options.preview) {
281
+ return {
282
+ success: true,
283
+ isPreview: true,
284
+ diff,
285
+ message: 'Preview complete (no changes applied)',
286
+ }
287
+ }
268
288
 
269
- console.log(`🤖 Agents Regenerated (${result.agents.length})`)
270
- console.log(`├── Workflow: ${workflowAgents.join(', ')}`)
271
- console.log(`└── Domain: ${domainAgents.join(', ') || 'none'}\n`)
289
+ // Interactive confirmation
290
+ const response = await prompts({
291
+ type: 'select',
292
+ name: 'action',
293
+ message: 'Apply these changes?',
294
+ choices: [
295
+ { title: 'Yes, apply changes', value: 'apply' },
296
+ { title: 'No, cancel', value: 'cancel' },
297
+ { title: 'Show full diff', value: 'diff' },
298
+ ],
299
+ })
300
+
301
+ if (response.action === 'cancel' || !response.action) {
302
+ out.warn('Sync cancelled')
303
+ return { success: false, message: 'Cancelled by user' }
304
+ }
272
305
 
273
- if (result.skills.length > 0) {
274
- console.log('📦 Skills Configured')
275
- for (const skill of result.skills) {
276
- console.log(`├── ${skill.agent}.md → ${skill.skill}`)
306
+ if (response.action === 'diff') {
307
+ console.log(`\n${formatFullDiff(diff)}`)
308
+ const confirm = await prompts({
309
+ type: 'confirm',
310
+ name: 'apply',
311
+ message: 'Apply these changes?',
312
+ initial: true,
313
+ })
314
+ if (!confirm.apply) {
315
+ out.warn('Sync cancelled')
316
+ return { success: false, message: 'Cancelled by user' }
317
+ }
277
318
  }
278
- console.log('')
319
+
320
+ // Changes already applied from dry-run, just show success
321
+ out.done('Changes applied')
322
+ return this.showSyncResult(result, startTime)
279
323
  }
280
324
 
281
- if (result.git.hasChanges) {
282
- console.log('⚠️ You have uncommitted changes\n')
283
- } else {
284
- console.log('✨ Repository is clean!\n')
325
+ // First sync or --yes flag - proceed directly
326
+ out.spin('Syncing project...')
327
+
328
+ // Use syncService to do EVERYTHING in one call
329
+ const result = await syncService.sync(projectPath, { aiTools: options.aiTools })
330
+
331
+ if (!result.success) {
332
+ out.fail(result.error || 'Sync failed')
333
+ return { success: false, error: result.error }
285
334
  }
286
335
 
287
- showNextSteps('sync')
336
+ out.stop()
337
+ return this.showSyncResult(result, startTime)
338
+ } catch (error) {
339
+ out.fail((error as Error).message)
340
+ return { success: false, error: (error as Error).message }
341
+ }
342
+ }
288
343
 
289
- // Summary metrics
290
- const elapsed = Date.now() - startTime
291
- const contextFilesCount =
292
- result.contextFiles.length + (result.aiTools?.filter((t) => t.success).length || 0)
293
- const agentCount = result.agents.length
344
+ /**
345
+ * Display sync results (extracted to avoid duplication)
346
+ */
347
+ private async showSyncResult(
348
+ result: Awaited<ReturnType<typeof syncService.sync>>,
349
+ startTime: number
350
+ ): Promise<CommandResult> {
351
+ // Update global config
352
+ const globalConfigResult = await commandInstaller.installGlobalConfig()
353
+ if (globalConfigResult.success) {
354
+ console.log(`📝 Updated ${pathManager.getDisplayPath(globalConfigResult.path!)}`)
355
+ }
294
356
 
295
- console.log('─'.repeat(45))
296
- console.log(`📊 Sync Summary`)
297
- console.log(
298
- ` Stack: ${result.stats.ecosystem} (${result.stats.frameworks.join(', ') || 'no frameworks'})`
299
- )
300
- console.log(
301
- ` Files: ${result.stats.fileCount} analyzed → ${contextFilesCount} context files`
302
- )
303
- console.log(
304
- ` Agents: ${agentCount} (${result.agents.filter((a) => a.type === 'domain').length} domain)`
305
- )
306
- console.log(` Time: ${(elapsed / 1000).toFixed(1)}s`)
357
+ // Format output
358
+ console.log(`🔄 Project synced to prjct v${result.cliVersion}\n`)
359
+
360
+ console.log('📊 Project Stats')
361
+ console.log(`├── Files: ~${result.stats.fileCount}`)
362
+ console.log(`├── Commits: ${result.git.commits}`)
363
+ console.log(`├── Version: ${result.stats.version}`)
364
+ console.log(`└── Stack: ${result.stats.ecosystem}\n`)
365
+
366
+ console.log('🌿 Git Status')
367
+ console.log(`├── Branch: ${result.git.branch}`)
368
+ console.log(`├── Uncommitted: ${result.git.hasChanges ? 'Yes' : 'Clean'}`)
369
+ console.log(`└── Recent: ${result.git.weeklyCommits} commits this week\n`)
370
+
371
+ console.log('📁 Context Updated')
372
+ for (const file of result.contextFiles) {
373
+ console.log(`├── ${file}`)
374
+ }
375
+ console.log('')
376
+
377
+ // Show AI Tools generated (multi-agent output)
378
+ if (result.aiTools && result.aiTools.length > 0) {
379
+ const successTools = result.aiTools.filter((t) => t.success)
380
+ console.log(`🤖 AI Tools Context (${successTools.length})`)
381
+ for (const tool of result.aiTools) {
382
+ const status = tool.success ? '✓' : '✗'
383
+ console.log(`├── ${status} ${tool.outputFile} (${tool.toolId})`)
384
+ }
307
385
  console.log('')
386
+ }
308
387
 
309
- return {
310
- success: true,
311
- data: result,
312
- metrics: {
313
- elapsed,
314
- contextFilesCount,
315
- agentCount,
316
- fileCount: result.stats.fileCount,
317
- },
388
+ const workflowAgents = result.agents.filter((a) => a.type === 'workflow').map((a) => a.name)
389
+ const domainAgents = result.agents.filter((a) => a.type === 'domain').map((a) => a.name)
390
+
391
+ console.log(`🤖 Agents Regenerated (${result.agents.length})`)
392
+ console.log(`├── Workflow: ${workflowAgents.join(', ')}`)
393
+ console.log(`└── Domain: ${domainAgents.join(', ') || 'none'}\n`)
394
+
395
+ if (result.skills.length > 0) {
396
+ console.log('📦 Skills Configured')
397
+ for (const skill of result.skills) {
398
+ console.log(`├── ${skill.agent}.md → ${skill.skill}`)
318
399
  }
319
- } catch (error) {
320
- console.error('❌ Error:', (error as Error).message)
321
- return { success: false, error: (error as Error).message }
400
+ console.log('')
401
+ }
402
+
403
+ if (result.git.hasChanges) {
404
+ console.log('⚠️ You have uncommitted changes\n')
405
+ } else {
406
+ console.log('✨ Repository is clean!\n')
407
+ }
408
+
409
+ showNextSteps('sync')
410
+
411
+ // Summary metrics
412
+ const elapsed = Date.now() - startTime
413
+ const contextFilesCount =
414
+ result.contextFiles.length + (result.aiTools?.filter((t) => t.success).length || 0)
415
+ const agentCount = result.agents.length
416
+
417
+ console.log('─'.repeat(45))
418
+ console.log('📊 Sync Summary')
419
+ console.log(
420
+ ` Stack: ${result.stats.ecosystem} (${result.stats.frameworks.join(', ') || 'no frameworks'})`
421
+ )
422
+ console.log(` Files: ${result.stats.fileCount} analyzed → ${contextFilesCount} context files`)
423
+ console.log(
424
+ ` Agents: ${agentCount} (${result.agents.filter((a) => a.type === 'domain').length} domain)`
425
+ )
426
+ console.log(` Time: ${(elapsed / 1000).toFixed(1)}s`)
427
+ console.log('')
428
+
429
+ return {
430
+ success: true,
431
+ data: result,
432
+ metrics: {
433
+ elapsed,
434
+ contextFilesCount,
435
+ agentCount,
436
+ fileCount: result.stats.fileCount,
437
+ },
322
438
  }
323
439
  }
324
440