prjct-cli 0.47.0 → 0.49.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 +52 -3821
- package/core/agentic/context-builder.ts +4 -4
- package/core/agentic/memory-system.ts +3 -1
- package/core/agentic/prompt-builder.ts +6 -2
- package/core/agentic/smart-context.ts +2 -2
- package/core/ai-tools/generator.ts +11 -1
- package/core/cli/linear.ts +5 -4
- package/core/commands/analytics.ts +4 -2
- package/core/commands/cleanup.ts +1 -1
- package/core/commands/commands.ts +1 -1
- package/core/commands/planning.ts +4 -4
- package/core/commands/registry.ts +3 -2
- package/core/commands/shipping.ts +1 -1
- package/core/commands/snapshots.ts +6 -6
- package/core/commands/workflow.ts +6 -6
- package/core/constants/index.ts +3 -1
- package/core/context-tools/imports-tool.ts +1 -1
- package/core/context-tools/signatures-tool.ts +1 -1
- package/core/plugin/hooks.ts +1 -1
- package/core/services/agent-generator.ts +58 -6
- package/core/services/context-generator.ts +35 -15
- package/core/utils/error-messages.ts +161 -0
- package/core/utils/help.ts +0 -1
- package/core/utils/markdown-builder.ts +9 -3
- package/core/utils/output.ts +24 -0
- package/core/utils/preserve-sections.ts +1 -1
- package/core/utils/project-commands.ts +0 -6
- package/core/utils/subtask-table.ts +1 -1
- package/dist/bin/prjct.mjs +308 -96
- package/package.json +1 -1
- package/templates/global/CLAUDE.md +27 -0
|
@@ -15,10 +15,10 @@ import { isNotFoundError } from '../types/fs'
|
|
|
15
15
|
// Re-export types for convenience
|
|
16
16
|
export type { ContextPaths, ContextState, ProjectContext } from '../types'
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
type Paths = ContextPaths
|
|
20
|
-
type Context = ProjectContext
|
|
21
|
-
type State = ContextState
|
|
18
|
+
// Type aliases exported for backward compatibility (used by external consumers)
|
|
19
|
+
export type Paths = ContextPaths
|
|
20
|
+
export type Context = ProjectContext
|
|
21
|
+
export type State = ContextState
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Builds and caches project context for Claude decisions.
|
|
@@ -574,7 +574,9 @@ export class SemanticMemories extends CachedStore<MemoryDatabase> {
|
|
|
574
574
|
const matchingIds = new Set<string>()
|
|
575
575
|
for (const tag of parsedTags) {
|
|
576
576
|
const ids = db.index[tag]
|
|
577
|
-
|
|
577
|
+
for (const id of ids) {
|
|
578
|
+
matchingIds.add(id)
|
|
579
|
+
}
|
|
578
580
|
}
|
|
579
581
|
return db.memories.filter((m) => matchingIds.has(m.id))
|
|
580
582
|
}
|
|
@@ -502,10 +502,14 @@ class PromptBuilder {
|
|
|
502
502
|
parts.push('\n## THINK FIRST (reasoning from analysis)\n')
|
|
503
503
|
if (thinkBlock.conclusions && thinkBlock.conclusions.length > 0) {
|
|
504
504
|
parts.push('Conclusions:\n')
|
|
505
|
-
|
|
505
|
+
for (const c of thinkBlock.conclusions) {
|
|
506
|
+
parts.push(` → ${c}\n`)
|
|
507
|
+
}
|
|
506
508
|
}
|
|
507
509
|
parts.push('Plan:\n')
|
|
508
|
-
|
|
510
|
+
for (let i = 0; i < thinkBlock.plan.length; i++) {
|
|
511
|
+
parts.push(` ${i + 1}. ${thinkBlock.plan[i]}\n`)
|
|
512
|
+
}
|
|
509
513
|
parts.push(`Confidence: ${Math.round((thinkBlock.confidence || 0.5) * 100)}%\n`)
|
|
510
514
|
}
|
|
511
515
|
|
|
@@ -32,8 +32,8 @@ export type {
|
|
|
32
32
|
StackInfo,
|
|
33
33
|
} from '../types'
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
type ProjectState = SmartContextProjectState
|
|
35
|
+
// Type alias exported for backward compatibility (used by external consumers)
|
|
36
|
+
export type ProjectState = SmartContextProjectState
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* SmartContext - Intelligent context filtering.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'node:fs/promises'
|
|
9
9
|
import path from 'node:path'
|
|
10
|
-
import { mergePreservedSections } from '../utils/preserve-sections'
|
|
10
|
+
import { mergePreservedSections, validatePreserveBlocks } from '../utils/preserve-sections'
|
|
11
11
|
import { getFormatter, type ProjectContext } from './formatters'
|
|
12
12
|
import { AI_TOOLS, type AIToolConfig, DEFAULT_AI_TOOLS, getAIToolConfig } from './registry'
|
|
13
13
|
|
|
@@ -88,6 +88,16 @@ async function generateForTool(
|
|
|
88
88
|
// Read existing file to preserve user customizations
|
|
89
89
|
try {
|
|
90
90
|
const existingContent = await fs.readFile(outputPath, 'utf-8')
|
|
91
|
+
|
|
92
|
+
// Validate existing preserved blocks
|
|
93
|
+
const validation = validatePreserveBlocks(existingContent)
|
|
94
|
+
if (!validation.valid) {
|
|
95
|
+
console.warn(`⚠️ ${config.outputFile} has invalid preserve blocks:`)
|
|
96
|
+
for (const error of validation.errors) {
|
|
97
|
+
console.warn(` ${error}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
91
101
|
content = mergePreservedSections(content, existingContent)
|
|
92
102
|
} catch {
|
|
93
103
|
// File doesn't exist yet - use generated content as-is
|
package/core/cli/linear.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* All output is JSON for easy parsing by Claude.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import type { CreateIssueInput, Issue } from '../integrations/issue-tracker/types'
|
|
27
28
|
import { linearService, linearSync } from '../integrations/linear'
|
|
28
29
|
import {
|
|
29
30
|
getCredentialSource,
|
|
@@ -140,7 +141,7 @@ async function main(): Promise<void> {
|
|
|
140
141
|
|
|
141
142
|
// Use team issues if teamId is configured, otherwise assigned issues
|
|
142
143
|
const creds = await getProjectCredentials(projectId!)
|
|
143
|
-
let issues
|
|
144
|
+
let issues: Issue[]
|
|
144
145
|
if (creds.linear?.teamId) {
|
|
145
146
|
issues = await linearService.fetchTeamIssues(creds.linear.teamId, { limit })
|
|
146
147
|
} else {
|
|
@@ -254,7 +255,7 @@ async function main(): Promise<void> {
|
|
|
254
255
|
error('JSON input required. Usage: create \'{"title":"...", "teamId":"..."}\'')
|
|
255
256
|
}
|
|
256
257
|
|
|
257
|
-
let input
|
|
258
|
+
let input: Record<string, unknown>
|
|
258
259
|
try {
|
|
259
260
|
input = JSON.parse(inputJson)
|
|
260
261
|
} catch {
|
|
@@ -274,7 +275,7 @@ async function main(): Promise<void> {
|
|
|
274
275
|
}
|
|
275
276
|
}
|
|
276
277
|
|
|
277
|
-
const issue = await linearService.createIssue(input)
|
|
278
|
+
const issue = await linearService.createIssue(input as unknown as CreateIssueInput)
|
|
278
279
|
output(issue)
|
|
279
280
|
break
|
|
280
281
|
}
|
|
@@ -291,7 +292,7 @@ async function main(): Promise<void> {
|
|
|
291
292
|
error('JSON input required. Usage: update <id> \'{"description":"..."}\'')
|
|
292
293
|
}
|
|
293
294
|
|
|
294
|
-
let input
|
|
295
|
+
let input: Record<string, unknown>
|
|
295
296
|
try {
|
|
296
297
|
input = JSON.parse(inputJson)
|
|
297
298
|
} catch {
|
|
@@ -39,7 +39,7 @@ export class AnalyticsCommands extends PrjctCommandsBase {
|
|
|
39
39
|
|
|
40
40
|
const projectId = await configManager.getProjectId(projectPath)
|
|
41
41
|
if (!projectId) {
|
|
42
|
-
out.
|
|
42
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
43
43
|
return { success: false, error: 'No project ID found' }
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -256,7 +256,9 @@ export class AnalyticsCommands extends PrjctCommandsBase {
|
|
|
256
256
|
|
|
257
257
|
if (command.features) {
|
|
258
258
|
console.log('\nFeatures:')
|
|
259
|
-
|
|
259
|
+
for (const f of command.features) {
|
|
260
|
+
console.log(` • ${f}`)
|
|
261
|
+
}
|
|
260
262
|
}
|
|
261
263
|
|
|
262
264
|
console.log(`\n${'═'.repeat(50)}\n`)
|
package/core/commands/cleanup.ts
CHANGED
|
@@ -254,7 +254,7 @@ export class PlanningCommands extends PrjctCommandsBase {
|
|
|
254
254
|
|
|
255
255
|
const projectId = await configManager.getProjectId(projectPath)
|
|
256
256
|
if (!projectId) {
|
|
257
|
-
out.
|
|
257
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
258
258
|
return { success: false, error: 'No project ID found' }
|
|
259
259
|
}
|
|
260
260
|
|
|
@@ -325,7 +325,7 @@ export class PlanningCommands extends PrjctCommandsBase {
|
|
|
325
325
|
|
|
326
326
|
const projectId = await configManager.getProjectId(projectPath)
|
|
327
327
|
if (!projectId) {
|
|
328
|
-
out.
|
|
328
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
329
329
|
return { success: false, error: 'No project ID found' }
|
|
330
330
|
}
|
|
331
331
|
|
|
@@ -485,7 +485,7 @@ export class PlanningCommands extends PrjctCommandsBase {
|
|
|
485
485
|
|
|
486
486
|
const projectId = await configManager.getProjectId(projectPath)
|
|
487
487
|
if (!projectId) {
|
|
488
|
-
out.
|
|
488
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
489
489
|
return { success: false, error: 'No project ID found' }
|
|
490
490
|
}
|
|
491
491
|
|
|
@@ -562,7 +562,7 @@ Generated: ${new Date().toLocaleString()}
|
|
|
562
562
|
|
|
563
563
|
const projectId = await configManager.getProjectId(projectPath)
|
|
564
564
|
if (!projectId) {
|
|
565
|
-
out.
|
|
565
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
566
566
|
return { success: false, error: 'No project ID found' }
|
|
567
567
|
}
|
|
568
568
|
|
|
@@ -117,10 +117,11 @@ export class CommandRegistry {
|
|
|
117
117
|
const wrapper: HandlerFn<unknown> = async (params, context) => {
|
|
118
118
|
// Legacy commands expect (param?, projectPath) signature
|
|
119
119
|
// Most commands use first param + projectPath
|
|
120
|
+
type LegacyMethod = (...args: unknown[]) => Promise<CommandResult>
|
|
120
121
|
if (params !== undefined && params !== null) {
|
|
121
|
-
return (method as
|
|
122
|
+
return (method as LegacyMethod).call(instance, params, context.projectPath)
|
|
122
123
|
}
|
|
123
|
-
return (method as
|
|
124
|
+
return (method as LegacyMethod).call(instance, context.projectPath)
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
this.handlerFns.set(name, wrapper)
|
|
@@ -60,7 +60,7 @@ export class ShippingCommands extends PrjctCommandsBase {
|
|
|
60
60
|
|
|
61
61
|
const projectId = await configManager.getProjectId(projectPath)
|
|
62
62
|
if (!projectId) {
|
|
63
|
-
out.
|
|
63
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
64
64
|
return { success: false, error: 'No project ID found' }
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -28,7 +28,7 @@ export async function recover(projectPath: string = process.cwd()): Promise<Comm
|
|
|
28
28
|
try {
|
|
29
29
|
const projectId = await configManager.getProjectId(projectPath)
|
|
30
30
|
if (!projectId) {
|
|
31
|
-
out.
|
|
31
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
32
32
|
return { success: false, error: 'No project ID found' }
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -85,7 +85,7 @@ export async function undo(projectPath: string = process.cwd()): Promise<Command
|
|
|
85
85
|
|
|
86
86
|
const projectId = await configManager.getProjectId(projectPath)
|
|
87
87
|
if (!projectId) {
|
|
88
|
-
out.
|
|
88
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
89
89
|
return { success: false, error: 'No project ID found' }
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -147,7 +147,7 @@ export async function undo(projectPath: string = process.cwd()): Promise<Command
|
|
|
147
147
|
out.done('changes stashed (use /p:redo to restore)')
|
|
148
148
|
return { success: true, snapshotId: stashMessage }
|
|
149
149
|
} catch (gitError) {
|
|
150
|
-
out.
|
|
150
|
+
out.failWithHint('GIT_OPERATION_FAILED')
|
|
151
151
|
return { success: false, error: (gitError as Error).message }
|
|
152
152
|
}
|
|
153
153
|
} catch (error) {
|
|
@@ -165,7 +165,7 @@ export async function redo(projectPath: string = process.cwd()): Promise<Command
|
|
|
165
165
|
|
|
166
166
|
const projectId = await configManager.getProjectId(projectPath)
|
|
167
167
|
if (!projectId) {
|
|
168
|
-
out.
|
|
168
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
169
169
|
return { success: false, error: 'No project ID found' }
|
|
170
170
|
}
|
|
171
171
|
|
|
@@ -231,7 +231,7 @@ export async function redo(projectPath: string = process.cwd()): Promise<Command
|
|
|
231
231
|
out.done('changes restored')
|
|
232
232
|
return { success: true }
|
|
233
233
|
} catch (gitError) {
|
|
234
|
-
out.
|
|
234
|
+
out.failWithHint('GIT_OPERATION_FAILED')
|
|
235
235
|
return { success: false, error: (gitError as Error).message }
|
|
236
236
|
}
|
|
237
237
|
} catch (error) {
|
|
@@ -247,7 +247,7 @@ export async function history(projectPath: string = process.cwd()): Promise<Comm
|
|
|
247
247
|
try {
|
|
248
248
|
const projectId = await configManager.getProjectId(projectPath)
|
|
249
249
|
if (!projectId) {
|
|
250
|
-
out.
|
|
250
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
251
251
|
return { success: false, error: 'No project ID found' }
|
|
252
252
|
}
|
|
253
253
|
|
|
@@ -43,7 +43,7 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
43
43
|
|
|
44
44
|
const projectId = await configManager.getProjectId(projectPath)
|
|
45
45
|
if (!projectId) {
|
|
46
|
-
out.
|
|
46
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
47
47
|
return { success: false, error: 'No project ID found' }
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -163,7 +163,7 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
163
163
|
|
|
164
164
|
const projectId = await configManager.getProjectId(projectPath)
|
|
165
165
|
if (!projectId) {
|
|
166
|
-
out.
|
|
166
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
167
167
|
return { success: false, error: 'No project ID found' }
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -246,7 +246,7 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
246
246
|
|
|
247
247
|
const projectId = await configManager.getProjectId(projectPath)
|
|
248
248
|
if (!projectId) {
|
|
249
|
-
out.
|
|
249
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
250
250
|
return { success: false, error: 'No project ID found' }
|
|
251
251
|
}
|
|
252
252
|
|
|
@@ -278,7 +278,7 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
278
278
|
|
|
279
279
|
const projectId = await configManager.getProjectId(projectPath)
|
|
280
280
|
if (!projectId) {
|
|
281
|
-
out.
|
|
281
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
282
282
|
return { success: false, error: 'No project ID found' }
|
|
283
283
|
}
|
|
284
284
|
|
|
@@ -323,7 +323,7 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
323
323
|
|
|
324
324
|
const projectId = await configManager.getProjectId(projectPath)
|
|
325
325
|
if (!projectId) {
|
|
326
|
-
out.
|
|
326
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
327
327
|
return { success: false, error: 'No project ID found' }
|
|
328
328
|
}
|
|
329
329
|
|
|
@@ -374,7 +374,7 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
374
374
|
|
|
375
375
|
const projectId = await configManager.getProjectId(projectPath)
|
|
376
376
|
if (!projectId) {
|
|
377
|
-
out.
|
|
377
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
378
378
|
return { success: false, error: 'No project ID found' }
|
|
379
379
|
}
|
|
380
380
|
|
package/core/constants/index.ts
CHANGED
|
@@ -108,7 +108,9 @@ export const ROADMAP = {
|
|
|
108
108
|
const lines = [`## ${feature}`, '', `Status: ${ROADMAP_STATUS[status]}`]
|
|
109
109
|
if (tasks && tasks.length > 0) {
|
|
110
110
|
lines.push('', '### Tasks', '')
|
|
111
|
-
|
|
111
|
+
for (const task of tasks) {
|
|
112
|
+
lines.push(`- [ ] ${task}`)
|
|
113
|
+
}
|
|
112
114
|
}
|
|
113
115
|
return `${lines.join('\n')}\n\n`
|
|
114
116
|
},
|
|
@@ -229,7 +229,7 @@ function extractImports(
|
|
|
229
229
|
for (const patternDef of patterns) {
|
|
230
230
|
patternDef.pattern.lastIndex = 0
|
|
231
231
|
|
|
232
|
-
let match
|
|
232
|
+
let match: RegExpExecArray | null
|
|
233
233
|
while ((match = patternDef.pattern.exec(content)) !== null) {
|
|
234
234
|
const source = match[patternDef.sourceIndex]
|
|
235
235
|
if (!source || seen.has(source)) continue
|
|
@@ -439,7 +439,7 @@ function extractFromContent(content: string, patterns: ExtractionPattern[]): Cod
|
|
|
439
439
|
// Reset lastIndex for global regex
|
|
440
440
|
patternDef.pattern.lastIndex = 0
|
|
441
441
|
|
|
442
|
-
let match
|
|
442
|
+
let match: RegExpExecArray | null
|
|
443
443
|
while ((match = patternDef.pattern.exec(content)) !== null) {
|
|
444
444
|
const name = match[patternDef.nameIndex]
|
|
445
445
|
if (!name) continue
|
package/core/plugin/hooks.ts
CHANGED
|
@@ -40,7 +40,7 @@ const HookPoints = {
|
|
|
40
40
|
TRANSFORM_METRICS: 'transform:metrics',
|
|
41
41
|
} as const
|
|
42
42
|
|
|
43
|
-
type HookPoint = (typeof HookPoints)[keyof typeof HookPoints]
|
|
43
|
+
export type HookPoint = (typeof HookPoints)[keyof typeof HookPoints]
|
|
44
44
|
type HookHandler = (data: unknown, context?: unknown) => unknown | Promise<unknown>
|
|
45
45
|
|
|
46
46
|
interface HookEntry {
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import fs from 'node:fs/promises'
|
|
11
11
|
import path from 'node:path'
|
|
12
|
+
import {
|
|
13
|
+
hasPreservedSections,
|
|
14
|
+
mergePreservedSections,
|
|
15
|
+
validatePreserveBlocks,
|
|
16
|
+
} from '../utils/preserve-sections'
|
|
12
17
|
import type { StackDetection } from './stack-detector'
|
|
13
18
|
|
|
14
19
|
// ============================================================================
|
|
@@ -65,21 +70,68 @@ export class AgentGenerator {
|
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/**
|
|
68
|
-
*
|
|
73
|
+
* Cache of existing agent content (for preserving user sections)
|
|
74
|
+
*/
|
|
75
|
+
private existingAgents: Map<string, string> = new Map()
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read existing agents and cache their content for preservation
|
|
79
|
+
* Then remove the files (they'll be regenerated with preserved sections)
|
|
69
80
|
*/
|
|
70
81
|
private async purgeOldAgents(): Promise<void> {
|
|
82
|
+
this.existingAgents.clear()
|
|
83
|
+
|
|
71
84
|
try {
|
|
72
85
|
const files = await fs.readdir(this.agentsPath)
|
|
86
|
+
const mdFiles = files.filter((file) => file.endsWith('.md'))
|
|
87
|
+
|
|
88
|
+
// Read all existing agent files BEFORE deleting
|
|
73
89
|
await Promise.all(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
mdFiles.map(async (file) => {
|
|
91
|
+
const filePath = path.join(this.agentsPath, file)
|
|
92
|
+
try {
|
|
93
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
94
|
+
// Only cache if it has user-preserved sections
|
|
95
|
+
if (hasPreservedSections(content)) {
|
|
96
|
+
this.existingAgents.set(file, content)
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// File read failed, skip
|
|
100
|
+
}
|
|
101
|
+
})
|
|
77
102
|
)
|
|
103
|
+
|
|
104
|
+
// Now delete the files
|
|
105
|
+
await Promise.all(mdFiles.map((file) => fs.unlink(path.join(this.agentsPath, file))))
|
|
78
106
|
} catch {
|
|
79
107
|
// Directory might not exist yet
|
|
80
108
|
}
|
|
81
109
|
}
|
|
82
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Write agent file, preserving user sections from previous version
|
|
113
|
+
*/
|
|
114
|
+
private async writeAgentWithPreservation(filename: string, content: string): Promise<void> {
|
|
115
|
+
const existingContent = this.existingAgents.get(filename)
|
|
116
|
+
|
|
117
|
+
let finalContent = content
|
|
118
|
+
if (existingContent) {
|
|
119
|
+
// Validate existing preserved blocks
|
|
120
|
+
const validation = validatePreserveBlocks(existingContent)
|
|
121
|
+
if (!validation.valid) {
|
|
122
|
+
console.warn(`⚠️ Agent ${filename} has invalid preserve blocks:`)
|
|
123
|
+
for (const error of validation.errors) {
|
|
124
|
+
console.warn(` ${error}`)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Merge preserved sections from old content
|
|
129
|
+
finalContent = mergePreservedSections(content, existingContent)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await fs.writeFile(path.join(this.agentsPath, filename), finalContent, 'utf-8')
|
|
133
|
+
}
|
|
134
|
+
|
|
83
135
|
/**
|
|
84
136
|
* Generate workflow agents (always included)
|
|
85
137
|
*/
|
|
@@ -143,7 +195,7 @@ export class AgentGenerator {
|
|
|
143
195
|
content = this.generateMinimalWorkflowAgent(name)
|
|
144
196
|
}
|
|
145
197
|
|
|
146
|
-
await
|
|
198
|
+
await this.writeAgentWithPreservation(`${name}.md`, content)
|
|
147
199
|
}
|
|
148
200
|
|
|
149
201
|
/**
|
|
@@ -169,7 +221,7 @@ export class AgentGenerator {
|
|
|
169
221
|
content = this.generateMinimalDomainAgent(name, stats, stack)
|
|
170
222
|
}
|
|
171
223
|
|
|
172
|
-
await
|
|
224
|
+
await this.writeAgentWithPreservation(`${name}.md`, content)
|
|
173
225
|
}
|
|
174
226
|
|
|
175
227
|
/**
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import fs from 'node:fs/promises'
|
|
13
13
|
import path from 'node:path'
|
|
14
14
|
import dateHelper from '../utils/date-helper'
|
|
15
|
-
import { mergePreservedSections } from '../utils/preserve-sections'
|
|
15
|
+
import { mergePreservedSections, validatePreserveBlocks } from '../utils/preserve-sections'
|
|
16
16
|
|
|
17
17
|
// ============================================================================
|
|
18
18
|
// TYPES
|
|
@@ -65,6 +65,35 @@ export class ContextFileGenerator {
|
|
|
65
65
|
this.config = config
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Write file with preserved sections from existing content
|
|
70
|
+
* This ensures user customizations survive regeneration
|
|
71
|
+
*/
|
|
72
|
+
private async writeWithPreservation(filePath: string, content: string): Promise<void> {
|
|
73
|
+
let finalContent = content
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const existingContent = await fs.readFile(filePath, 'utf-8')
|
|
77
|
+
|
|
78
|
+
// Validate existing preserved blocks
|
|
79
|
+
const validation = validatePreserveBlocks(existingContent)
|
|
80
|
+
if (!validation.valid) {
|
|
81
|
+
const filename = path.basename(filePath)
|
|
82
|
+
console.warn(`⚠️ ${filename} has invalid preserve blocks:`)
|
|
83
|
+
for (const error of validation.errors) {
|
|
84
|
+
console.warn(` ${error}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Merge preserved sections from existing content
|
|
89
|
+
finalContent = mergePreservedSections(content, existingContent)
|
|
90
|
+
} catch {
|
|
91
|
+
// File doesn't exist yet - use generated content as-is
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await fs.writeFile(filePath, finalContent, 'utf-8')
|
|
95
|
+
}
|
|
96
|
+
|
|
68
97
|
/**
|
|
69
98
|
* Generate all context files in parallel
|
|
70
99
|
*/
|
|
@@ -181,17 +210,8 @@ Load from \`~/.prjct-cli/projects/${this.config.projectId}/agents/\`:
|
|
|
181
210
|
**Domain**: ${domainAgents.join(', ') || 'none'}
|
|
182
211
|
`
|
|
183
212
|
|
|
184
|
-
// Preserve user customizations from existing file
|
|
185
213
|
const claudePath = path.join(contextPath, 'CLAUDE.md')
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
const existingContent = await fs.readFile(claudePath, 'utf-8')
|
|
189
|
-
finalContent = mergePreservedSections(content, existingContent)
|
|
190
|
-
} catch {
|
|
191
|
-
// File doesn't exist yet - use generated content as-is
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
await fs.writeFile(claudePath, finalContent, 'utf-8')
|
|
214
|
+
await this.writeWithPreservation(claudePath, content)
|
|
195
215
|
}
|
|
196
216
|
|
|
197
217
|
/**
|
|
@@ -222,7 +242,7 @@ _No active task_
|
|
|
222
242
|
Use \`p. task "description"\` to start working.
|
|
223
243
|
`
|
|
224
244
|
|
|
225
|
-
await
|
|
245
|
+
await this.writeWithPreservation(path.join(contextPath, 'now.md'), content)
|
|
226
246
|
}
|
|
227
247
|
|
|
228
248
|
/**
|
|
@@ -248,7 +268,7 @@ ${
|
|
|
248
268
|
}
|
|
249
269
|
`
|
|
250
270
|
|
|
251
|
-
await
|
|
271
|
+
await this.writeWithPreservation(path.join(contextPath, 'next.md'), content)
|
|
252
272
|
}
|
|
253
273
|
|
|
254
274
|
/**
|
|
@@ -272,7 +292,7 @@ ${
|
|
|
272
292
|
}
|
|
273
293
|
`
|
|
274
294
|
|
|
275
|
-
await
|
|
295
|
+
await this.writeWithPreservation(path.join(contextPath, 'ideas.md'), content)
|
|
276
296
|
}
|
|
277
297
|
|
|
278
298
|
/**
|
|
@@ -303,7 +323,7 @@ ${
|
|
|
303
323
|
**Total shipped:** ${shipped.shipped.length}
|
|
304
324
|
`
|
|
305
325
|
|
|
306
|
-
await
|
|
326
|
+
await this.writeWithPreservation(path.join(contextPath, 'shipped.md'), content)
|
|
307
327
|
}
|
|
308
328
|
}
|
|
309
329
|
|