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 +80 -1
- package/core/__tests__/ai-tools/formatters.test.ts +118 -0
- package/core/ai-tools/formatters.ts +18 -0
- package/core/index.ts +91 -10
- package/core/services/context-generator.ts +12 -3
- package/core/services/sync-service.ts +61 -3
- package/core/utils/citations.ts +53 -0
- package/core/utils/error-messages.ts +11 -0
- package/dist/bin/prjct.mjs +172 -19
- package/package.json +1 -1
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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',
|
package/dist/bin/prjct.mjs
CHANGED
|
@@ -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
|
|
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(
|
|
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.
|
|
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
|
-
|
|
27947
|
-
|
|
27948
|
-
|
|
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
|
-
|
|
27954
|
-
|
|
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
|
-
|
|
27962
|
-
|
|
27963
|
-
|
|
27964
|
-
|
|
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");
|