prjct-cli 1.4.0 → 1.5.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,12 +1,91 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.0] - 2026-02-06
4
+
5
+ ### Features
6
+
7
+ - add citation format for context sources in templates (PRJ-113) (#117)
8
+
9
+
10
+ ## [1.4.2] - 2026-02-06
11
+
12
+ ### Features
13
+
14
+ - **Source citations in context files (PRJ-113)**: All generated context files now include `<!-- source: file, type -->` HTML comments showing where each section's data was detected from
15
+
16
+ ### Implementation Details
17
+
18
+ - Added `SourceInfo` type and `ContextSources` interface with `cite()` helper (`core/utils/citations.ts`)
19
+ - Extended `ProjectContext` with optional `sources` field — backward compatible, falls back to `defaultSources()`
20
+ - Added `buildSources()` to `sync-service.ts` — maps detected ecosystem/commands data to their source files (package.json, lock files, Cargo.toml, etc.)
21
+ - Citations added to 4 markdown formatters: Claude, Cursor, Windsurf, Copilot. Continue.dev skipped (JSON has no comment syntax)
22
+ - Context generator CLAUDE.md also updated with citation support
23
+ - Source types: `detected` (from files), `user-defined` (from config), `inferred` (from heuristics)
24
+
25
+ ### Learnings
26
+
27
+ - `context-generator.ts` and `formatters.ts` both independently generate CLAUDE.md content — both must be updated for consistent citations
28
+ - Sources can be determined post-detection from data values rather than threading metadata through every detection method
29
+ - Optional fields with fallback defaults (`sources?`) maintain backward compatibility without breaking existing callers
30
+
31
+ ### Test Plan
32
+
33
+ #### For QA
34
+ 1. Run `prjct sync --yes` — verify generated context files contain `<!-- source: ... -->` comments
35
+ 2. Check CLAUDE.md citations before: THIS PROJECT, Commands, Code Conventions, PROJECT STATE
36
+ 3. Check `.cursor/rules/prjct.mdc` citations before Tech Stack and Commands
37
+ 4. Check `.windsurf/rules/prjct.md` citations before Stack and Commands
38
+ 5. Check `.github/copilot-instructions.md` citations before Project Info and Commands
39
+ 6. Verify `.continue/config.json` unchanged (JSON has no comments)
40
+ 7. Run `bun test` — all 416 tests pass
41
+
42
+ #### For Users
43
+ **What changed:** Context files now show where data came from via HTML comments
44
+ **How to use:** Run `p. sync` — citations appear automatically
45
+ **Breaking changes:** None
46
+
47
+ ## [1.4.1] - 2026-02-06
48
+
49
+ ### Improvements
50
+
51
+ - **Better error messages for invalid commands (PRJ-98)**: Consistent, helpful CLI errors with did-you-mean suggestions and required parameter validation
52
+
53
+ ### Implementation Details
54
+
55
+ - Added `UNKNOWN_COMMAND` and `MISSING_PARAM` error codes to centralized error catalog (`core/utils/error-messages.ts`)
56
+ - Added `validateCommandParams()` — parses `CommandMeta.params` convention (`<required>` vs `[optional]`) and validates against actual CLI args before command execution
57
+ - Added `findClosestCommand()` with Levenshtein edit distance (threshold ≤ 2) for did-you-mean suggestions on typos
58
+ - All error paths now use `out.failWithHint()` for consistent formatting with hints, docs links, and file references
59
+ - Deprecated and unimplemented command errors also upgraded to use `failWithHint()`
60
+
61
+ ### Learnings
62
+
63
+ - `bin/prjct.ts` intercepts many commands (start, context, hooks, doctor, etc.) before `core/index.ts` — changes to dispatch only affect commands that reach core
64
+ - Template-only commands (e.g. `task`) are defined in `command-data.ts` but not registered in the command registry — they don't get param validation via CLI
65
+ - Levenshtein edit distance is simple to implement (~15 lines) and effective for CLI typo suggestions
66
+
67
+ ### Test Plan
68
+
69
+ #### For QA
70
+ 1. `prjct xyzzy` → "Unknown command: xyzzy" with help hint
71
+ 2. `prjct snyc` → "Did you mean 'prjct sync'?"
72
+ 3. `prjct shp` → "Did you mean 'prjct ship'?"
73
+ 4. `prjct bug` (no args) → "Missing required parameter: description" with usage
74
+ 5. `prjct idea` (no args) → "Missing required parameter: description" with usage
75
+ 6. `prjct sync --yes` → works normally (no regression)
76
+ 7. `prjct dash compact` → works normally (no regression)
77
+
78
+ #### For Users
79
+ **What changed:** CLI now shows helpful error messages with suggestions when you mistype a command or forget a required argument.
80
+ **How to use:** Just use prjct normally — errors are now more helpful automatically.
81
+ **Breaking changes:** None
82
+
3
83
  ## [1.4.0] - 2026-02-06
4
84
 
5
85
  ### Features
6
86
 
7
87
  - programmatic verification checks for sync workflow (PRJ-106) (#115)
8
88
 
9
-
10
89
  ## [1.3.1] - 2026-02-06
11
90
 
12
91
  ### Features
@@ -2,6 +2,7 @@
2
2
  * Tests for AI Tools Formatters
3
3
  *
4
4
  * @see PRJ-122
5
+ * @see PRJ-113 (citation support)
5
6
  */
6
7
 
7
8
  import { describe, expect, test } from 'bun:test'
@@ -15,6 +16,7 @@ import {
15
16
  type ProjectContext,
16
17
  } from '../../ai-tools/formatters'
17
18
  import { AI_TOOLS, getAIToolConfig } from '../../ai-tools/registry'
19
+ import { type ContextSources, cite, defaultSources } from '../../utils/citations'
18
20
 
19
21
  // =============================================================================
20
22
  // Test Fixtures
@@ -356,3 +358,119 @@ describe('Formatter Edge Cases', () => {
356
358
  expect(claudeResult).toContain('@scope/my-project')
357
359
  })
358
360
  })
361
+
362
+ // =============================================================================
363
+ // Citation Tests (PRJ-113)
364
+ // =============================================================================
365
+
366
+ const mockSources: ContextSources = {
367
+ name: { file: 'package.json', type: 'detected' },
368
+ version: { file: 'package.json', type: 'detected' },
369
+ ecosystem: { file: 'package.json', type: 'detected' },
370
+ languages: { file: 'package.json', type: 'detected' },
371
+ frameworks: { file: 'package.json', type: 'detected' },
372
+ commands: { file: 'bun.lockb', type: 'detected' },
373
+ projectType: { file: 'file count + frameworks', type: 'inferred' },
374
+ git: { file: 'git', type: 'detected' },
375
+ }
376
+
377
+ const ctxWithSources: ProjectContext = {
378
+ ...mockContext,
379
+ sources: mockSources,
380
+ }
381
+
382
+ describe('cite() helper', () => {
383
+ test('generates HTML comment with file and type', () => {
384
+ const result = cite({ file: 'package.json', type: 'detected' })
385
+ expect(result).toBe('<!-- source: package.json, detected -->')
386
+ })
387
+
388
+ test('supports inferred type', () => {
389
+ const result = cite({ file: 'file count + frameworks', type: 'inferred' })
390
+ expect(result).toBe('<!-- source: file count + frameworks, inferred -->')
391
+ })
392
+
393
+ test('supports user-defined type', () => {
394
+ const result = cite({ file: 'prjct.yaml', type: 'user-defined' })
395
+ expect(result).toBe('<!-- source: prjct.yaml, user-defined -->')
396
+ })
397
+ })
398
+
399
+ describe('defaultSources()', () => {
400
+ test('returns all source fields', () => {
401
+ const sources = defaultSources()
402
+ expect(sources.name).toBeDefined()
403
+ expect(sources.version).toBeDefined()
404
+ expect(sources.ecosystem).toBeDefined()
405
+ expect(sources.languages).toBeDefined()
406
+ expect(sources.frameworks).toBeDefined()
407
+ expect(sources.commands).toBeDefined()
408
+ expect(sources.projectType).toBeDefined()
409
+ expect(sources.git).toBeDefined()
410
+ })
411
+
412
+ test('git source is always "git"', () => {
413
+ const sources = defaultSources()
414
+ expect(sources.git.file).toBe('git')
415
+ expect(sources.git.type).toBe('detected')
416
+ })
417
+ })
418
+
419
+ describe('Citation integration', () => {
420
+ test('Claude format includes source citations', () => {
421
+ const result = formatForClaude(ctxWithSources, AI_TOOLS.claude)
422
+
423
+ expect(result).toContain('<!-- source: package.json, detected -->')
424
+ expect(result).toContain('<!-- source: bun.lockb, detected -->')
425
+ })
426
+
427
+ test('Cursor format includes source citations', () => {
428
+ const result = formatForCursor(ctxWithSources, AI_TOOLS.cursor)
429
+
430
+ expect(result).toContain('<!-- source: package.json, detected -->')
431
+ expect(result).toContain('<!-- source: bun.lockb, detected -->')
432
+ })
433
+
434
+ test('Windsurf format includes source citations', () => {
435
+ const result = formatForWindsurf(ctxWithSources, AI_TOOLS.windsurf)
436
+
437
+ expect(result).toContain('<!-- source: package.json, detected -->')
438
+ expect(result).toContain('<!-- source: bun.lockb, detected -->')
439
+ })
440
+
441
+ test('Copilot format includes source citations', () => {
442
+ const result = formatForCopilot(ctxWithSources, AI_TOOLS.copilot)
443
+
444
+ expect(result).toContain('<!-- source: package.json, detected -->')
445
+ expect(result).toContain('<!-- source: bun.lockb, detected -->')
446
+ })
447
+
448
+ test('formatters work without sources (backward compatible)', () => {
449
+ const ctxNoSources = { ...mockContext }
450
+ delete ctxNoSources.sources
451
+
452
+ // Should not throw
453
+ expect(() => formatForClaude(ctxNoSources, AI_TOOLS.claude)).not.toThrow()
454
+ expect(() => formatForCursor(ctxNoSources, AI_TOOLS.cursor)).not.toThrow()
455
+ expect(() => formatForWindsurf(ctxNoSources, AI_TOOLS.windsurf)).not.toThrow()
456
+ expect(() => formatForCopilot(ctxNoSources, AI_TOOLS.copilot)).not.toThrow()
457
+
458
+ // Should still contain default citation comments
459
+ const result = formatForClaude(ctxNoSources, AI_TOOLS.claude)
460
+ expect(result).toContain('<!-- source:')
461
+ })
462
+
463
+ test('Claude format has citations before each major section', () => {
464
+ const result = formatForClaude(ctxWithSources, AI_TOOLS.claude)
465
+
466
+ // Ecosystem citation before project type
467
+ const ecosystemIdx = result.indexOf('<!-- source: package.json, detected -->')
468
+ const projectTypeIdx = result.indexOf('**Type:**')
469
+ expect(ecosystemIdx).toBeLessThan(projectTypeIdx)
470
+
471
+ // Commands citation before commands table
472
+ const commandsCiteIdx = result.indexOf('<!-- source: bun.lockb, detected -->')
473
+ const commandsTableIdx = result.indexOf('| Action | Command |')
474
+ expect(commandsCiteIdx).toBeLessThan(commandsTableIdx)
475
+ })
476
+ })
@@ -8,6 +8,7 @@
8
8
  * - Windsurf: Similar to Cursor
9
9
  */
10
10
 
11
+ import { type ContextSources, cite, defaultSources } from '../utils/citations'
11
12
  import type { AIToolConfig } from './registry'
12
13
 
13
14
  export interface ProjectContext {
@@ -35,6 +36,7 @@ export interface ProjectContext {
35
36
  workflow: string[]
36
37
  domain: string[]
37
38
  }
39
+ sources?: ContextSources
38
40
  }
39
41
 
40
42
  /**
@@ -42,6 +44,8 @@ export interface ProjectContext {
42
44
  * Detailed markdown with full context
43
45
  */
44
46
  export function formatForClaude(ctx: ProjectContext, _config: AIToolConfig): string {
47
+ const s = ctx.sources || defaultSources()
48
+
45
49
  return `# ${ctx.name} - Project Rules
46
50
  <!-- projectId: ${ctx.projectId} -->
47
51
  <!-- Generated: ${new Date().toISOString()} -->
@@ -49,11 +53,13 @@ export function formatForClaude(ctx: ProjectContext, _config: AIToolConfig): str
49
53
 
50
54
  ## THIS PROJECT (${ctx.ecosystem})
51
55
 
56
+ ${cite(s.ecosystem)}
52
57
  **Type:** ${ctx.projectType}
53
58
  **Path:** ${ctx.repoPath}
54
59
 
55
60
  ### Commands (USE THESE, NOT OTHERS)
56
61
 
62
+ ${cite(s.commands)}
57
63
  | Action | Command |
58
64
  |--------|---------|
59
65
  | Install dependencies | \`${ctx.commands.install}\` |
@@ -65,7 +71,9 @@ export function formatForClaude(ctx: ProjectContext, _config: AIToolConfig): str
65
71
 
66
72
  ### Code Conventions
67
73
 
74
+ ${cite(s.languages)}
68
75
  - **Languages**: ${ctx.languages.join(', ') || 'Not detected'}
76
+ ${cite(s.frameworks)}
69
77
  - **Frameworks**: ${ctx.frameworks.join(', ') || 'Not detected'}
70
78
 
71
79
  ---
@@ -93,6 +101,7 @@ p. sync → p. task "desc" → [work] → p. done → p. ship
93
101
 
94
102
  ## PROJECT STATE
95
103
 
104
+ ${cite(s.name)}
96
105
  | Field | Value |
97
106
  |-------|-------|
98
107
  | Name | ${ctx.name} |
@@ -120,6 +129,7 @@ Load from \`~/.prjct-cli/projects/${ctx.projectId}/agents/\`:
120
129
  * @see https://cursor.com/docs/context/rules
121
130
  */
122
131
  export function formatForCursor(ctx: ProjectContext, _config: AIToolConfig): string {
132
+ const s = ctx.sources || defaultSources()
123
133
  const lines: string[] = []
124
134
 
125
135
  // MDC format with YAML frontmatter
@@ -135,6 +145,7 @@ export function formatForCursor(ctx: ProjectContext, _config: AIToolConfig): str
135
145
  lines.push('')
136
146
 
137
147
  // Tech stack
148
+ lines.push(cite(s.languages))
138
149
  lines.push('## Tech Stack')
139
150
  if (ctx.languages.length > 0) {
140
151
  lines.push(`- Languages: ${ctx.languages.join(', ')}`)
@@ -145,6 +156,7 @@ export function formatForCursor(ctx: ProjectContext, _config: AIToolConfig): str
145
156
  lines.push('')
146
157
 
147
158
  // Commands
159
+ lines.push(cite(s.commands))
148
160
  lines.push('## Commands')
149
161
  lines.push(`- Install: \`${ctx.commands.install}\``)
150
162
  lines.push(`- Dev: \`${ctx.commands.dev}\``)
@@ -175,6 +187,7 @@ export function formatForCursor(ctx: ProjectContext, _config: AIToolConfig): str
175
187
  * Minimal bullet points
176
188
  */
177
189
  export function formatForCopilot(ctx: ProjectContext, _config: AIToolConfig): string {
190
+ const s = ctx.sources || defaultSources()
178
191
  const lines: string[] = []
179
192
 
180
193
  lines.push('# Copilot Instructions')
@@ -183,6 +196,7 @@ export function formatForCopilot(ctx: ProjectContext, _config: AIToolConfig): st
183
196
  lines.push('')
184
197
 
185
198
  // Key info
199
+ lines.push(cite(s.ecosystem))
186
200
  lines.push('## Project Info')
187
201
  lines.push(`- Type: ${ctx.projectType}`)
188
202
  lines.push(`- Stack: ${ctx.frameworks.join(', ') || ctx.ecosystem}`)
@@ -196,6 +210,7 @@ export function formatForCopilot(ctx: ProjectContext, _config: AIToolConfig): st
196
210
  lines.push('')
197
211
 
198
212
  // Commands
213
+ lines.push(cite(s.commands))
199
214
  lines.push('## Commands')
200
215
  lines.push(`- Test: \`${ctx.commands.test}\``)
201
216
  lines.push(`- Build: \`${ctx.commands.build}\``)
@@ -210,6 +225,7 @@ export function formatForCopilot(ctx: ProjectContext, _config: AIToolConfig): st
210
225
  * @see https://docs.windsurf.com/windsurf/cascade/memories
211
226
  */
212
227
  export function formatForWindsurf(ctx: ProjectContext, _config: AIToolConfig): string {
228
+ const s = ctx.sources || defaultSources()
213
229
  const lines: string[] = []
214
230
 
215
231
  // YAML frontmatter (Windsurf uses trigger: always_on instead of alwaysApply)
@@ -226,6 +242,7 @@ export function formatForWindsurf(ctx: ProjectContext, _config: AIToolConfig): s
226
242
  lines.push('')
227
243
 
228
244
  // Tech stack (concise)
245
+ lines.push(cite(s.languages))
229
246
  lines.push('## Stack')
230
247
  lines.push(`- ${ctx.languages.join(', ')}`)
231
248
  if (ctx.frameworks.length > 0) {
@@ -234,6 +251,7 @@ export function formatForWindsurf(ctx: ProjectContext, _config: AIToolConfig): s
234
251
  lines.push('')
235
252
 
236
253
  // Commands (essential only)
254
+ lines.push(cite(s.commands))
237
255
  lines.push('## Commands')
238
256
  lines.push('```bash')
239
257
  lines.push(`# Install`)
package/core/index.ts CHANGED
@@ -15,6 +15,7 @@ import type { CommandMeta } from './commands/registry'
15
15
  import { detectAllProviders, detectAntigravity } from './infrastructure/ai-provider'
16
16
  import configManager from './infrastructure/config-manager'
17
17
  import { sessionTracker } from './services/session-tracker'
18
+ import { getError } from './utils/error-messages'
18
19
  import out from './utils/output'
19
20
 
20
21
  interface ParsedCommandArgs {
@@ -53,27 +54,34 @@ async function main(): Promise<void> {
53
54
  const cmd = commandRegistry.getByName(commandName)
54
55
 
55
56
  if (!cmd) {
56
- console.error(`Unknown command: ${commandName}`)
57
- console.error(`\nUse 'prjct --help' to see available commands.`)
57
+ const suggestion = findClosestCommand(commandName)
58
+ const hint = suggestion
59
+ ? `Did you mean 'prjct ${suggestion}'? Run 'prjct --help' for all commands`
60
+ : "Run 'prjct --help' to see available commands"
61
+ out.failWithHint(
62
+ getError('UNKNOWN_COMMAND', { message: `Unknown command: ${commandName}`, hint })
63
+ )
58
64
  out.end()
59
65
  process.exit(1)
60
66
  }
61
67
 
62
68
  // 2. Check if deprecated
63
69
  if (cmd.deprecated) {
64
- console.error(`Command '${commandName}' is deprecated.`)
65
- if (cmd.replacedBy) {
66
- console.error(`Use 'prjct ${cmd.replacedBy}' instead.`)
67
- }
70
+ const hint = cmd.replacedBy
71
+ ? `Use 'prjct ${cmd.replacedBy}' instead`
72
+ : "Run 'prjct --help' to see available commands"
73
+ out.failWithHint({ message: `Command '${commandName}' is deprecated`, hint })
68
74
  out.end()
69
75
  process.exit(1)
70
76
  }
71
77
 
72
78
  // 3. Check if implemented
73
79
  if (!cmd.implemented) {
74
- console.error(`Command '${commandName}' exists but is not yet implemented.`)
75
- console.error(`Check the roadmap or contribute: https://github.com/jlopezlira/prjct-cli`)
76
- console.error(`\nUse 'prjct --help' to see available commands.`)
80
+ out.failWithHint({
81
+ message: `Command '${commandName}' is not yet implemented`,
82
+ hint: "Run 'prjct --help' to see available commands",
83
+ docs: 'https://github.com/jlopezlira/prjct-cli',
84
+ })
77
85
  out.end()
78
86
  process.exit(1)
79
87
  }
@@ -81,7 +89,15 @@ async function main(): Promise<void> {
81
89
  // 4. Parse arguments
82
90
  const { parsedArgs, options } = parseCommandArgs(cmd, rawArgs)
83
91
 
84
- // 4.5. Session tracking — touch/create session before command execution
92
+ // 4.5. Validate required params
93
+ const paramError = validateCommandParams(cmd, parsedArgs)
94
+ if (paramError) {
95
+ out.failWithHint(paramError)
96
+ out.end()
97
+ process.exit(1)
98
+ }
99
+
100
+ // 4.6. Session tracking — touch/create session before command execution
85
101
  let projectId: string | null = null
86
102
  const commandStartTime = Date.now()
87
103
  try {
@@ -193,6 +209,71 @@ async function main(): Promise<void> {
193
209
  }
194
210
  }
195
211
 
212
+ /**
213
+ * Validate that required params are provided
214
+ * Parses CommandMeta.params: <required> vs [optional]
215
+ */
216
+ function validateCommandParams(
217
+ cmd: CommandMeta,
218
+ parsedArgs: string[]
219
+ ): import('./utils/error-messages').ErrorWithHint | null {
220
+ if (!cmd.params) return null
221
+
222
+ // Extract required params: tokens wrapped in <angle brackets>
223
+ const requiredParams = cmd.params.match(/<[^>]+>/g)
224
+ if (!requiredParams || requiredParams.length === 0) return null
225
+
226
+ // Check if enough positional args provided
227
+ if (parsedArgs.length < requiredParams.length) {
228
+ const paramNames = requiredParams.map((p) => p.slice(1, -1)).join(', ')
229
+ const usage = cmd.usage.terminal || `prjct ${cmd.name} ${cmd.params}`
230
+ return getError('MISSING_PARAM', {
231
+ message: `Missing required parameter: ${paramNames}`,
232
+ hint: `Usage: ${usage}`,
233
+ })
234
+ }
235
+
236
+ return null
237
+ }
238
+
239
+ /**
240
+ * Find closest matching command name for did-you-mean suggestions
241
+ * Uses Levenshtein edit distance — suggests if distance <= 2
242
+ */
243
+ function findClosestCommand(input: string): string | null {
244
+ const allNames = commandRegistry.getAll().map((c) => c.name)
245
+ let best: string | null = null
246
+ let bestDist = Infinity
247
+
248
+ for (const name of allNames) {
249
+ const dist = editDistance(input.toLowerCase(), name.toLowerCase())
250
+ if (dist < bestDist) {
251
+ bestDist = dist
252
+ best = name
253
+ }
254
+ }
255
+
256
+ // Only suggest if edit distance is at most 2
257
+ return bestDist <= 2 ? best : null
258
+ }
259
+
260
+ function editDistance(a: string, b: string): number {
261
+ const m = a.length
262
+ const n = b.length
263
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))
264
+ for (let i = 0; i <= m; i++) dp[i][0] = i
265
+ for (let j = 0; j <= n; j++) dp[0][j] = j
266
+ for (let i = 1; i <= m; i++) {
267
+ for (let j = 1; j <= n; j++) {
268
+ dp[i][j] =
269
+ a[i - 1] === b[j - 1]
270
+ ? dp[i - 1][j - 1]
271
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
272
+ }
273
+ }
274
+ return dp[m][n]
275
+ }
276
+
196
277
  /**
197
278
  * Parse command arguments dynamically
198
279
  */
@@ -12,6 +12,7 @@
12
12
  import fs from 'node:fs/promises'
13
13
  import path from 'node:path'
14
14
  import pathManager from '../infrastructure/path-manager'
15
+ import { type ContextSources, cite, defaultSources } from '../utils/citations'
15
16
  import dateHelper from '../utils/date-helper'
16
17
  import { mergePreservedSections, validatePreserveBlocks } from '../utils/preserve-sections'
17
18
  import { NestedContextResolver } from './nested-context-resolver'
@@ -103,13 +104,14 @@ export class ContextFileGenerator {
103
104
  git: GitData,
104
105
  stats: ProjectStats,
105
106
  commands: Commands,
106
- agents: AgentInfo[]
107
+ agents: AgentInfo[],
108
+ sources?: ContextSources
107
109
  ): Promise<string[]> {
108
110
  const contextPath = path.join(this.config.globalPath, 'context')
109
111
 
110
112
  // Generate all context files IN PARALLEL
111
113
  await Promise.all([
112
- this.generateClaudeMd(contextPath, git, stats, commands, agents),
114
+ this.generateClaudeMd(contextPath, git, stats, commands, agents, sources),
113
115
  this.generateNowMd(contextPath),
114
116
  this.generateNextMd(contextPath),
115
117
  this.generateIdeasMd(contextPath),
@@ -137,10 +139,12 @@ export class ContextFileGenerator {
137
139
  git: GitData,
138
140
  stats: ProjectStats,
139
141
  commands: Commands,
140
- agents: AgentInfo[]
142
+ agents: AgentInfo[],
143
+ sources?: ContextSources
141
144
  ): Promise<void> {
142
145
  const workflowAgents = agents.filter((a) => a.type === 'workflow').map((a) => a.name)
143
146
  const domainAgents = agents.filter((a) => a.type === 'domain').map((a) => a.name)
147
+ const s = sources || defaultSources()
144
148
 
145
149
  const content = `# ${stats.name} - Project Rules
146
150
  <!-- projectId: ${this.config.projectId} -->
@@ -149,11 +153,13 @@ export class ContextFileGenerator {
149
153
 
150
154
  ## THIS PROJECT (${stats.ecosystem})
151
155
 
156
+ ${cite(s.ecosystem)}
152
157
  **Type:** ${stats.projectType}
153
158
  **Path:** ${this.config.projectPath}
154
159
 
155
160
  ### Commands (USE THESE, NOT OTHERS)
156
161
 
162
+ ${cite(s.commands)}
157
163
  | Action | Command |
158
164
  |--------|---------|
159
165
  | Install dependencies | \`${commands.install}\` |
@@ -165,7 +171,9 @@ export class ContextFileGenerator {
165
171
 
166
172
  ### Code Conventions
167
173
 
174
+ ${cite(s.languages)}
168
175
  - **Languages**: ${stats.languages.join(', ') || 'Not detected'}
176
+ ${cite(s.frameworks)}
169
177
  - **Frameworks**: ${stats.frameworks.join(', ') || 'Not detected'}
170
178
 
171
179
  ---
@@ -193,6 +201,7 @@ p. sync → p. task "desc" → [work] → p. done → p. ship
193
201
 
194
202
  ## PROJECT STATE
195
203
 
204
+ ${cite(s.name)}
196
205
  | Field | Value |
197
206
  |-------|-------|
198
207
  | Name | ${stats.name} |
@@ -30,6 +30,7 @@ import commandInstaller from '../infrastructure/command-installer'
30
30
  import configManager from '../infrastructure/config-manager'
31
31
  import pathManager from '../infrastructure/path-manager'
32
32
  import { metricsStorage } from '../storage/metrics-storage'
33
+ import { type ContextSources, defaultSources, type SourceInfo } from '../utils/citations'
33
34
  import dateHelper from '../utils/date-helper'
34
35
  import { ContextFileGenerator } from './context-generator'
35
36
  import type { SyncDiff } from './diff-generator'
@@ -202,7 +203,8 @@ class SyncService {
202
203
  // 4. Generate all files (depends on gathered data)
203
204
  const agents = await this.generateAgents(stack, stats)
204
205
  const skills = this.configureSkills(agents)
205
- const contextFiles = await this.generateContextFiles(git, stats, commands, agents)
206
+ const sources = this.buildSources(stats, commands)
207
+ const contextFiles = await this.generateContextFiles(git, stats, commands, agents, sources)
206
208
 
207
209
  // 5. Generate AI tool context files (multi-agent output)
208
210
  const projectContext: ProjectContext = {
@@ -223,6 +225,7 @@ class SyncService {
223
225
  workflow: agents.filter((a) => a.type === 'workflow').map((a) => a.name),
224
226
  domain: agents.filter((a) => a.type === 'domain').map((a) => a.name),
225
227
  },
228
+ sources,
226
229
  }
227
230
 
228
231
  const aiToolResults = await generateAIToolContexts(
@@ -535,6 +538,54 @@ class SyncService {
535
538
  return commands
536
539
  }
537
540
 
541
+ // ==========================================================================
542
+ // SOURCE CITATIONS
543
+ // ==========================================================================
544
+
545
+ private buildSources(stats: ProjectStats, commands: Commands): ContextSources {
546
+ const sources = defaultSources()
547
+
548
+ // Determine ecosystem source file
549
+ const ecosystemFiles: Record<string, string> = {
550
+ JavaScript: 'package.json',
551
+ Rust: 'Cargo.toml',
552
+ Go: 'go.mod',
553
+ Python: 'pyproject.toml',
554
+ }
555
+ const ecosystemFile = ecosystemFiles[stats.ecosystem] || 'filesystem'
556
+ const detected = (file: string): SourceInfo => ({ file, type: 'detected' })
557
+ const inferred = (file: string): SourceInfo => ({ file, type: 'inferred' })
558
+
559
+ sources.ecosystem = detected(ecosystemFile)
560
+ sources.name = detected(ecosystemFile)
561
+ sources.version = detected(ecosystemFile)
562
+ sources.languages = detected(ecosystemFile)
563
+ sources.frameworks = detected(ecosystemFile)
564
+
565
+ // Commands source is the lock file or ecosystem file
566
+ if (commands.install.startsWith('bun')) {
567
+ sources.commands = detected('bun.lockb')
568
+ } else if (commands.install.startsWith('pnpm')) {
569
+ sources.commands = detected('pnpm-lock.yaml')
570
+ } else if (commands.install === 'yarn') {
571
+ sources.commands = detected('yarn.lock')
572
+ } else if (commands.install.startsWith('cargo')) {
573
+ sources.commands = detected('Cargo.toml')
574
+ } else if (commands.install.startsWith('go')) {
575
+ sources.commands = detected('go.mod')
576
+ } else {
577
+ sources.commands = detected('package.json')
578
+ }
579
+
580
+ // Project type is inferred from file count + framework count
581
+ sources.projectType = inferred('file count + frameworks')
582
+
583
+ // Git is always from git
584
+ sources.git = detected('git')
585
+
586
+ return sources
587
+ }
588
+
538
589
  // ==========================================================================
539
590
  // STACK DETECTION
540
591
  // ==========================================================================
@@ -757,7 +808,8 @@ You are the ${name} expert for this project. Apply best practices for the detect
757
808
  git: GitData,
758
809
  stats: ProjectStats,
759
810
  commands: Commands,
760
- agents: AgentInfo[]
811
+ agents: AgentInfo[],
812
+ sources?: ContextSources
761
813
  ): Promise<string[]> {
762
814
  const generator = new ContextFileGenerator({
763
815
  projectId: this.projectId!,
@@ -765,7 +817,13 @@ You are the ${name} expert for this project. Apply best practices for the detect
765
817
  globalPath: this.globalPath,
766
818
  })
767
819
 
768
- return generator.generate({ branch: git.branch, commits: git.commits }, stats, commands, agents)
820
+ return generator.generate(
821
+ { branch: git.branch, commits: git.commits },
822
+ stats,
823
+ commands,
824
+ agents,
825
+ sources
826
+ )
769
827
  }
770
828
 
771
829
  // ==========================================================================
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Citation utilities for context source tracking
3
+ *
4
+ * Generates HTML comments indicating where each section's data came from.
5
+ * Source types: detected (from files), user-defined (from config), inferred (from heuristics)
6
+ *
7
+ * @see PRJ-113
8
+ */
9
+
10
+ export type SourceType = 'detected' | 'user-defined' | 'inferred'
11
+
12
+ export interface SourceInfo {
13
+ file: string
14
+ type: SourceType
15
+ }
16
+
17
+ export interface ContextSources {
18
+ name: SourceInfo
19
+ version: SourceInfo
20
+ ecosystem: SourceInfo
21
+ languages: SourceInfo
22
+ frameworks: SourceInfo
23
+ commands: SourceInfo
24
+ projectType: SourceInfo
25
+ git: SourceInfo
26
+ }
27
+
28
+ /**
29
+ * Generate an HTML citation comment
30
+ *
31
+ * @example cite({ file: 'package.json', type: 'detected' })
32
+ * // => '<!-- source: package.json, detected -->'
33
+ */
34
+ export function cite(source: SourceInfo): string {
35
+ return `<!-- source: ${source.file}, ${source.type} -->`
36
+ }
37
+
38
+ /**
39
+ * Create default sources (all unknown) - used as fallback
40
+ */
41
+ export function defaultSources(): ContextSources {
42
+ const unknown: SourceInfo = { file: 'unknown', type: 'detected' }
43
+ return {
44
+ name: { ...unknown },
45
+ version: { ...unknown },
46
+ ecosystem: { ...unknown },
47
+ languages: { ...unknown },
48
+ frameworks: { ...unknown },
49
+ commands: { ...unknown },
50
+ projectType: { ...unknown },
51
+ git: { file: 'git', type: 'detected' },
52
+ }
53
+ }
@@ -128,6 +128,17 @@ export const ERRORS = {
128
128
  hint: "Run 'prjct start' to configure your provider",
129
129
  },
130
130
 
131
+ // Command errors
132
+ UNKNOWN_COMMAND: {
133
+ message: 'Unknown command',
134
+ hint: "Run 'prjct --help' to see available commands",
135
+ },
136
+
137
+ MISSING_PARAM: {
138
+ message: 'Missing required parameter',
139
+ hint: 'Check command usage below',
140
+ },
141
+
131
142
  // Generic
132
143
  UNKNOWN: {
133
144
  message: 'An unexpected error occurred',
@@ -2225,6 +2225,15 @@ var init_error_messages = __esm({
2225
2225
  message: "AI provider not configured for prjct",
2226
2226
  hint: "Run 'prjct start' to configure your provider"
2227
2227
  },
2228
+ // Command errors
2229
+ UNKNOWN_COMMAND: {
2230
+ message: "Unknown command",
2231
+ hint: "Run 'prjct --help' to see available commands"
2232
+ },
2233
+ MISSING_PARAM: {
2234
+ message: "Missing required parameter",
2235
+ hint: "Check command usage below"
2236
+ },
2228
2237
  // Generic
2229
2238
  UNKNOWN: {
2230
2239
  message: "An unexpected error occurred",
@@ -20378,8 +20387,34 @@ var init_staleness_checker = __esm({
20378
20387
  }
20379
20388
  });
20380
20389
 
20390
+ // core/utils/citations.ts
20391
+ function cite(source) {
20392
+ return `<!-- source: ${source.file}, ${source.type} -->`;
20393
+ }
20394
+ function defaultSources() {
20395
+ const unknown = { file: "unknown", type: "detected" };
20396
+ return {
20397
+ name: { ...unknown },
20398
+ version: { ...unknown },
20399
+ ecosystem: { ...unknown },
20400
+ languages: { ...unknown },
20401
+ frameworks: { ...unknown },
20402
+ commands: { ...unknown },
20403
+ projectType: { ...unknown },
20404
+ git: { file: "git", type: "detected" }
20405
+ };
20406
+ }
20407
+ var init_citations = __esm({
20408
+ "core/utils/citations.ts"() {
20409
+ "use strict";
20410
+ __name(cite, "cite");
20411
+ __name(defaultSources, "defaultSources");
20412
+ }
20413
+ });
20414
+
20381
20415
  // core/ai-tools/formatters.ts
20382
20416
  function formatForClaude(ctx, _config) {
20417
+ const s = ctx.sources || defaultSources();
20383
20418
  return `# ${ctx.name} - Project Rules
20384
20419
  <!-- projectId: ${ctx.projectId} -->
20385
20420
  <!-- Generated: ${(/* @__PURE__ */ new Date()).toISOString()} -->
@@ -20387,11 +20422,13 @@ function formatForClaude(ctx, _config) {
20387
20422
 
20388
20423
  ## THIS PROJECT (${ctx.ecosystem})
20389
20424
 
20425
+ ${cite(s.ecosystem)}
20390
20426
  **Type:** ${ctx.projectType}
20391
20427
  **Path:** ${ctx.repoPath}
20392
20428
 
20393
20429
  ### Commands (USE THESE, NOT OTHERS)
20394
20430
 
20431
+ ${cite(s.commands)}
20395
20432
  | Action | Command |
20396
20433
  |--------|---------|
20397
20434
  | Install dependencies | \`${ctx.commands.install}\` |
@@ -20403,7 +20440,9 @@ function formatForClaude(ctx, _config) {
20403
20440
 
20404
20441
  ### Code Conventions
20405
20442
 
20443
+ ${cite(s.languages)}
20406
20444
  - **Languages**: ${ctx.languages.join(", ") || "Not detected"}
20445
+ ${cite(s.frameworks)}
20407
20446
  - **Frameworks**: ${ctx.frameworks.join(", ") || "Not detected"}
20408
20447
 
20409
20448
  ---
@@ -20431,6 +20470,7 @@ p. sync \u2192 p. task "desc" \u2192 [work] \u2192 p. done \u2192 p. ship
20431
20470
 
20432
20471
  ## PROJECT STATE
20433
20472
 
20473
+ ${cite(s.name)}
20434
20474
  | Field | Value |
20435
20475
  |-------|-------|
20436
20476
  | Name | ${ctx.name} |
@@ -20451,6 +20491,7 @@ Load from \`~/.prjct-cli/projects/${ctx.projectId}/agents/\`:
20451
20491
  `;
20452
20492
  }
20453
20493
  function formatForCursor(ctx, _config) {
20494
+ const s = ctx.sources || defaultSources();
20454
20495
  const lines = [];
20455
20496
  lines.push("---");
20456
20497
  lines.push(`description: prjct context for ${ctx.name}`);
@@ -20460,6 +20501,7 @@ function formatForCursor(ctx, _config) {
20460
20501
  lines.push("");
20461
20502
  lines.push(`You are working on ${ctx.name}, a ${ctx.projectType} ${ctx.ecosystem} project.`);
20462
20503
  lines.push("");
20504
+ lines.push(cite(s.languages));
20463
20505
  lines.push("## Tech Stack");
20464
20506
  if (ctx.languages.length > 0) {
20465
20507
  lines.push(`- Languages: ${ctx.languages.join(", ")}`);
@@ -20468,6 +20510,7 @@ function formatForCursor(ctx, _config) {
20468
20510
  lines.push(`- Frameworks: ${ctx.frameworks.join(", ")}`);
20469
20511
  }
20470
20512
  lines.push("");
20513
+ lines.push(cite(s.commands));
20471
20514
  lines.push("## Commands");
20472
20515
  lines.push(`- Install: \`${ctx.commands.install}\``);
20473
20516
  lines.push(`- Dev: \`${ctx.commands.dev}\``);
@@ -20488,11 +20531,13 @@ function formatForCursor(ctx, _config) {
20488
20531
  return lines.join("\n");
20489
20532
  }
20490
20533
  function formatForCopilot(ctx, _config) {
20534
+ const s = ctx.sources || defaultSources();
20491
20535
  const lines = [];
20492
20536
  lines.push("# Copilot Instructions");
20493
20537
  lines.push("");
20494
20538
  lines.push(`This is ${ctx.name}, a ${ctx.ecosystem} project.`);
20495
20539
  lines.push("");
20540
+ lines.push(cite(s.ecosystem));
20496
20541
  lines.push("## Project Info");
20497
20542
  lines.push(`- Type: ${ctx.projectType}`);
20498
20543
  lines.push(`- Stack: ${ctx.frameworks.join(", ") || ctx.ecosystem}`);
@@ -20502,12 +20547,14 @@ function formatForCopilot(ctx, _config) {
20502
20547
  lines.push("- Match existing code patterns");
20503
20548
  lines.push("- Keep code clean and readable");
20504
20549
  lines.push("");
20550
+ lines.push(cite(s.commands));
20505
20551
  lines.push("## Commands");
20506
20552
  lines.push(`- Test: \`${ctx.commands.test}\``);
20507
20553
  lines.push(`- Build: \`${ctx.commands.build}\``);
20508
20554
  return lines.join("\n");
20509
20555
  }
20510
20556
  function formatForWindsurf(ctx, _config) {
20557
+ const s = ctx.sources || defaultSources();
20511
20558
  const lines = [];
20512
20559
  lines.push("---");
20513
20560
  lines.push(`description: prjct context for ${ctx.name}`);
@@ -20518,12 +20565,14 @@ function formatForWindsurf(ctx, _config) {
20518
20565
  lines.push("");
20519
20566
  lines.push(`${ctx.projectType} project using ${ctx.ecosystem}.`);
20520
20567
  lines.push("");
20568
+ lines.push(cite(s.languages));
20521
20569
  lines.push("## Stack");
20522
20570
  lines.push(`- ${ctx.languages.join(", ")}`);
20523
20571
  if (ctx.frameworks.length > 0) {
20524
20572
  lines.push(`- ${ctx.frameworks.join(", ")}`);
20525
20573
  }
20526
20574
  lines.push("");
20575
+ lines.push(cite(s.commands));
20527
20576
  lines.push("## Commands");
20528
20577
  lines.push("```bash");
20529
20578
  lines.push(`# Install`);
@@ -20597,6 +20646,7 @@ function getFormatter(toolId) {
20597
20646
  var init_formatters = __esm({
20598
20647
  "core/ai-tools/formatters.ts"() {
20599
20648
  "use strict";
20649
+ init_citations();
20600
20650
  __name(formatForClaude, "formatForClaude");
20601
20651
  __name(formatForCursor, "formatForCursor");
20602
20652
  __name(formatForCopilot, "formatForCopilot");
@@ -20811,6 +20861,7 @@ var init_context_generator = __esm({
20811
20861
  "core/services/context-generator.ts"() {
20812
20862
  "use strict";
20813
20863
  init_path_manager();
20864
+ init_citations();
20814
20865
  init_date_helper();
20815
20866
  init_preserve_sections();
20816
20867
  init_nested_context_resolver();
@@ -20846,10 +20897,10 @@ var init_context_generator = __esm({
20846
20897
  /**
20847
20898
  * Generate all context files in parallel
20848
20899
  */
20849
- async generate(git, stats, commands, agents) {
20900
+ async generate(git, stats, commands, agents, sources) {
20850
20901
  const contextPath = path43.join(this.config.globalPath, "context");
20851
20902
  await Promise.all([
20852
- this.generateClaudeMd(contextPath, git, stats, commands, agents),
20903
+ this.generateClaudeMd(contextPath, git, stats, commands, agents, sources),
20853
20904
  this.generateNowMd(contextPath),
20854
20905
  this.generateNextMd(contextPath),
20855
20906
  this.generateIdeasMd(contextPath),
@@ -20869,9 +20920,10 @@ var init_context_generator = __esm({
20869
20920
  /**
20870
20921
  * Generate CLAUDE.md - main context file for AI agents
20871
20922
  */
20872
- async generateClaudeMd(contextPath, git, stats, commands, agents) {
20923
+ async generateClaudeMd(contextPath, git, stats, commands, agents, sources) {
20873
20924
  const workflowAgents = agents.filter((a) => a.type === "workflow").map((a) => a.name);
20874
20925
  const domainAgents = agents.filter((a) => a.type === "domain").map((a) => a.name);
20926
+ const s = sources || defaultSources();
20875
20927
  const content = `# ${stats.name} - Project Rules
20876
20928
  <!-- projectId: ${this.config.projectId} -->
20877
20929
  <!-- Generated: ${date_helper_default.getTimestamp()} -->
@@ -20879,11 +20931,13 @@ var init_context_generator = __esm({
20879
20931
 
20880
20932
  ## THIS PROJECT (${stats.ecosystem})
20881
20933
 
20934
+ ${cite(s.ecosystem)}
20882
20935
  **Type:** ${stats.projectType}
20883
20936
  **Path:** ${this.config.projectPath}
20884
20937
 
20885
20938
  ### Commands (USE THESE, NOT OTHERS)
20886
20939
 
20940
+ ${cite(s.commands)}
20887
20941
  | Action | Command |
20888
20942
  |--------|---------|
20889
20943
  | Install dependencies | \`${commands.install}\` |
@@ -20895,7 +20949,9 @@ var init_context_generator = __esm({
20895
20949
 
20896
20950
  ### Code Conventions
20897
20951
 
20952
+ ${cite(s.languages)}
20898
20953
  - **Languages**: ${stats.languages.join(", ") || "Not detected"}
20954
+ ${cite(s.frameworks)}
20899
20955
  - **Frameworks**: ${stats.frameworks.join(", ") || "Not detected"}
20900
20956
 
20901
20957
  ---
@@ -20923,6 +20979,7 @@ p. sync \u2192 p. task "desc" \u2192 [work] \u2192 p. done \u2192 p. ship
20923
20979
 
20924
20980
  ## PROJECT STATE
20925
20981
 
20982
+ ${cite(s.name)}
20926
20983
  | Field | Value |
20927
20984
  |-------|-------|
20928
20985
  | Name | ${stats.name} |
@@ -21637,6 +21694,7 @@ var init_sync_service = __esm({
21637
21694
  init_config_manager();
21638
21695
  init_path_manager();
21639
21696
  init_metrics_storage();
21697
+ init_citations();
21640
21698
  init_date_helper();
21641
21699
  init_context_generator();
21642
21700
  init_local_state_generator();
@@ -21704,7 +21762,8 @@ var init_sync_service = __esm({
21704
21762
  await ensureDirsPromise;
21705
21763
  const agents = await this.generateAgents(stack, stats);
21706
21764
  const skills = this.configureSkills(agents);
21707
- const contextFiles = await this.generateContextFiles(git, stats, commands, agents);
21765
+ const sources = this.buildSources(stats, commands);
21766
+ const contextFiles = await this.generateContextFiles(git, stats, commands, agents, sources);
21708
21767
  const projectContext = {
21709
21768
  projectId: this.projectId,
21710
21769
  name: stats.name,
@@ -21722,7 +21781,8 @@ var init_sync_service = __esm({
21722
21781
  agents: {
21723
21782
  workflow: agents.filter((a) => a.type === "workflow").map((a) => a.name),
21724
21783
  domain: agents.filter((a) => a.type === "domain").map((a) => a.name)
21725
- }
21784
+ },
21785
+ sources
21726
21786
  };
21727
21787
  const aiToolResults = await generateAIToolContexts(
21728
21788
  projectContext,
@@ -21975,6 +22035,42 @@ var init_sync_service = __esm({
21975
22035
  return commands;
21976
22036
  }
21977
22037
  // ==========================================================================
22038
+ // SOURCE CITATIONS
22039
+ // ==========================================================================
22040
+ buildSources(stats, commands) {
22041
+ const sources = defaultSources();
22042
+ const ecosystemFiles = {
22043
+ JavaScript: "package.json",
22044
+ Rust: "Cargo.toml",
22045
+ Go: "go.mod",
22046
+ Python: "pyproject.toml"
22047
+ };
22048
+ const ecosystemFile = ecosystemFiles[stats.ecosystem] || "filesystem";
22049
+ const detected = /* @__PURE__ */ __name((file) => ({ file, type: "detected" }), "detected");
22050
+ const inferred = /* @__PURE__ */ __name((file) => ({ file, type: "inferred" }), "inferred");
22051
+ sources.ecosystem = detected(ecosystemFile);
22052
+ sources.name = detected(ecosystemFile);
22053
+ sources.version = detected(ecosystemFile);
22054
+ sources.languages = detected(ecosystemFile);
22055
+ sources.frameworks = detected(ecosystemFile);
22056
+ if (commands.install.startsWith("bun")) {
22057
+ sources.commands = detected("bun.lockb");
22058
+ } else if (commands.install.startsWith("pnpm")) {
22059
+ sources.commands = detected("pnpm-lock.yaml");
22060
+ } else if (commands.install === "yarn") {
22061
+ sources.commands = detected("yarn.lock");
22062
+ } else if (commands.install.startsWith("cargo")) {
22063
+ sources.commands = detected("Cargo.toml");
22064
+ } else if (commands.install.startsWith("go")) {
22065
+ sources.commands = detected("go.mod");
22066
+ } else {
22067
+ sources.commands = detected("package.json");
22068
+ }
22069
+ sources.projectType = inferred("file count + frameworks");
22070
+ sources.git = detected("git");
22071
+ return sources;
22072
+ }
22073
+ // ==========================================================================
21978
22074
  // STACK DETECTION
21979
22075
  // ==========================================================================
21980
22076
  async detectStack() {
@@ -22144,13 +22240,19 @@ You are the ${name} expert for this project. Apply best practices for the detect
22144
22240
  // ==========================================================================
22145
22241
  // CONTEXT FILE GENERATION
22146
22242
  // ==========================================================================
22147
- async generateContextFiles(git, stats, commands, agents) {
22243
+ async generateContextFiles(git, stats, commands, agents, sources) {
22148
22244
  const generator = new ContextFileGenerator({
22149
22245
  projectId: this.projectId,
22150
22246
  projectPath: this.projectPath,
22151
22247
  globalPath: this.globalPath
22152
22248
  });
22153
- return generator.generate({ branch: git.branch, commits: git.commits }, stats, commands, agents);
22249
+ return generator.generate(
22250
+ { branch: git.branch, commits: git.commits },
22251
+ stats,
22252
+ commands,
22253
+ agents,
22254
+ sources
22255
+ );
22154
22256
  }
22155
22257
  // ==========================================================================
22156
22258
  // PROJECT.JSON UPDATE
@@ -27817,7 +27919,7 @@ var require_package = __commonJS({
27817
27919
  "package.json"(exports, module) {
27818
27920
  module.exports = {
27819
27921
  name: "prjct-cli",
27820
- version: "1.4.0",
27922
+ version: "1.5.0",
27821
27923
  description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
27822
27924
  main: "core/index.ts",
27823
27925
  bin: {
@@ -27943,29 +28045,36 @@ async function main() {
27943
28045
  try {
27944
28046
  const cmd = commandRegistry.getByName(commandName);
27945
28047
  if (!cmd) {
27946
- console.error(`Unknown command: ${commandName}`);
27947
- console.error(`
27948
- Use 'prjct --help' to see available commands.`);
28048
+ const suggestion = findClosestCommand(commandName);
28049
+ const hint = suggestion ? `Did you mean 'prjct ${suggestion}'? Run 'prjct --help' for all commands` : "Run 'prjct --help' to see available commands";
28050
+ output_default.failWithHint(
28051
+ getError("UNKNOWN_COMMAND", { message: `Unknown command: ${commandName}`, hint })
28052
+ );
27949
28053
  output_default.end();
27950
28054
  process.exit(1);
27951
28055
  }
27952
28056
  if (cmd.deprecated) {
27953
- console.error(`Command '${commandName}' is deprecated.`);
27954
- if (cmd.replacedBy) {
27955
- console.error(`Use 'prjct ${cmd.replacedBy}' instead.`);
27956
- }
28057
+ const hint = cmd.replacedBy ? `Use 'prjct ${cmd.replacedBy}' instead` : "Run 'prjct --help' to see available commands";
28058
+ output_default.failWithHint({ message: `Command '${commandName}' is deprecated`, hint });
27957
28059
  output_default.end();
27958
28060
  process.exit(1);
27959
28061
  }
27960
28062
  if (!cmd.implemented) {
27961
- console.error(`Command '${commandName}' exists but is not yet implemented.`);
27962
- console.error(`Check the roadmap or contribute: https://github.com/jlopezlira/prjct-cli`);
27963
- console.error(`
27964
- Use 'prjct --help' to see available commands.`);
28063
+ output_default.failWithHint({
28064
+ message: `Command '${commandName}' is not yet implemented`,
28065
+ hint: "Run 'prjct --help' to see available commands",
28066
+ docs: "https://github.com/jlopezlira/prjct-cli"
28067
+ });
27965
28068
  output_default.end();
27966
28069
  process.exit(1);
27967
28070
  }
27968
28071
  const { parsedArgs, options } = parseCommandArgs(cmd, rawArgs);
28072
+ const paramError = validateCommandParams(cmd, parsedArgs);
28073
+ if (paramError) {
28074
+ output_default.failWithHint(paramError);
28075
+ output_default.end();
28076
+ process.exit(1);
28077
+ }
27969
28078
  let projectId = null;
27970
28079
  const commandStartTime = Date.now();
27971
28080
  try {
@@ -28056,6 +28165,46 @@ Use 'prjct --help' to see available commands.`);
28056
28165
  process.exit(1);
28057
28166
  }
28058
28167
  }
28168
+ function validateCommandParams(cmd, parsedArgs) {
28169
+ if (!cmd.params) return null;
28170
+ const requiredParams = cmd.params.match(/<[^>]+>/g);
28171
+ if (!requiredParams || requiredParams.length === 0) return null;
28172
+ if (parsedArgs.length < requiredParams.length) {
28173
+ const paramNames = requiredParams.map((p) => p.slice(1, -1)).join(", ");
28174
+ const usage = cmd.usage.terminal || `prjct ${cmd.name} ${cmd.params}`;
28175
+ return getError("MISSING_PARAM", {
28176
+ message: `Missing required parameter: ${paramNames}`,
28177
+ hint: `Usage: ${usage}`
28178
+ });
28179
+ }
28180
+ return null;
28181
+ }
28182
+ function findClosestCommand(input) {
28183
+ const allNames = commandRegistry.getAll().map((c) => c.name);
28184
+ let best = null;
28185
+ let bestDist = Infinity;
28186
+ for (const name of allNames) {
28187
+ const dist = editDistance(input.toLowerCase(), name.toLowerCase());
28188
+ if (dist < bestDist) {
28189
+ bestDist = dist;
28190
+ best = name;
28191
+ }
28192
+ }
28193
+ return bestDist <= 2 ? best : null;
28194
+ }
28195
+ function editDistance(a, b) {
28196
+ const m = a.length;
28197
+ const n = b.length;
28198
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
28199
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
28200
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
28201
+ for (let i = 1; i <= m; i++) {
28202
+ for (let j = 1; j <= n; j++) {
28203
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
28204
+ }
28205
+ }
28206
+ return dp[m][n];
28207
+ }
28059
28208
  function parseCommandArgs(_cmd, rawArgs) {
28060
28209
  const parsedArgs = [];
28061
28210
  const options = {};
@@ -28192,8 +28341,12 @@ var init_core = __esm({
28192
28341
  init_ai_provider();
28193
28342
  init_config_manager();
28194
28343
  init_session_tracker();
28344
+ init_error_messages();
28195
28345
  init_output();
28196
28346
  __name(main, "main");
28347
+ __name(validateCommandParams, "validateCommandParams");
28348
+ __name(findClosestCommand, "findClosestCommand");
28349
+ __name(editDistance, "editDistance");
28197
28350
  __name(parseCommandArgs, "parseCommandArgs");
28198
28351
  __name(displayVersion, "displayVersion");
28199
28352
  __name(displayHelp, "displayHelp");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
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": {