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.
@@ -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
- // Local type aliases for backward compatibility
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
- ids.forEach((id: string) => matchingIds.add(id))
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
- thinkBlock.conclusions.forEach((c) => parts.push(` → ${c}\n`))
505
+ for (const c of thinkBlock.conclusions) {
506
+ parts.push(` → ${c}\n`)
507
+ }
506
508
  }
507
509
  parts.push('Plan:\n')
508
- thinkBlock.plan.forEach((p, i) => parts.push(` ${i + 1}. ${p}\n`))
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
- // Local type alias for backward compatibility
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
@@ -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.fail('no project ID')
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
- command.features.forEach((f) => console.log(` • ${f}`))
259
+ for (const f of command.features) {
260
+ console.log(` • ${f}`)
261
+ }
260
262
  }
261
263
 
262
264
  console.log(`\n${'═'.repeat(50)}\n`)
@@ -79,7 +79,7 @@ export async function cleanup(
79
79
 
80
80
  const projectId = await configManager.getProjectId(projectPath)
81
81
  if (!projectId) {
82
- out.fail('no project ID')
82
+ out.failWithHint('NO_PROJECT_ID')
83
83
  return { success: false, error: 'No project ID found' }
84
84
  }
85
85
 
@@ -220,7 +220,7 @@ class PrjctCommands {
220
220
  }
221
221
 
222
222
  showAsciiArt(): void {
223
- return this.setupCmds.showAsciiArt()
223
+ this.setupCmds.showAsciiArt()
224
224
  }
225
225
 
226
226
  // ========== Delegated Base Methods ==========
@@ -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.fail('no project ID')
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.fail('no project ID')
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.fail('no project ID')
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.fail('no project ID')
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 Function).call(instance, params, context.projectPath)
122
+ return (method as LegacyMethod).call(instance, params, context.projectPath)
122
123
  }
123
- return (method as Function).call(instance, context.projectPath)
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.fail('no project ID')
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.fail('no project ID')
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.fail('no project ID')
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.fail('git operation failed')
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.fail('no project ID')
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.fail('git operation failed')
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.fail('no project ID')
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.fail('no project ID')
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.fail('no project ID')
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.fail('no project ID')
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.fail('no project ID')
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.fail('no project ID')
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.fail('no project ID')
377
+ out.failWithHint('NO_PROJECT_ID')
378
378
  return { success: false, error: 'No project ID found' }
379
379
  }
380
380
 
@@ -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
- tasks.forEach((task) => lines.push(`- [ ] ${task}`))
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
@@ -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
- * Remove existing agent files
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
- files
75
- .filter((file) => file.endsWith('.md'))
76
- .map((file) => fs.unlink(path.join(this.agentsPath, file)))
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 fs.writeFile(path.join(this.agentsPath, `${name}.md`), content, 'utf-8')
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 fs.writeFile(path.join(this.agentsPath, `${name}.md`), content, 'utf-8')
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
- let finalContent = content
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 fs.writeFile(path.join(contextPath, 'now.md'), content, 'utf-8')
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 fs.writeFile(path.join(contextPath, 'next.md'), content, 'utf-8')
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 fs.writeFile(path.join(contextPath, 'ideas.md'), content, 'utf-8')
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 fs.writeFile(path.join(contextPath, 'shipped.md'), content, 'utf-8')
326
+ await this.writeWithPreservation(path.join(contextPath, 'shipped.md'), content)
307
327
  }
308
328
  }
309
329