prjct-cli 0.46.0 → 0.48.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/bin/prjct.ts CHANGED
@@ -184,53 +184,9 @@ if (args[0] === 'start' || args[0] === 'setup') {
184
184
  }
185
185
  } else if (args[0] === 'help' || args[0] === '-h' || args[0] === '--help') {
186
186
  // Show help - bypass setup check to always show help
187
- console.log(`
188
- prjct - Context layer for AI coding agents
189
- Works with Claude Code, Gemini CLI, Antigravity, Cursor IDE, and more.
190
-
191
- QUICK START
192
- -----------
193
- Claude/Gemini:
194
- 1. prjct start Configure your AI provider
195
- 2. cd my-project && prjct init
196
- 3. Open in Claude Code or Gemini CLI
197
- 4. Type: p. sync Analyze project
198
-
199
- Cursor IDE:
200
- 1. cd my-project && prjct init
201
- 2. Open in Cursor
202
- 3. Type: /sync Analyze project
203
-
204
- COMMANDS (inside your AI agent)
205
- -------------------------------
206
- Claude/Gemini Cursor Description
207
- ─────────────────────────────────────────────────────
208
- p. sync /sync Analyze project
209
- p. task "desc" /task "desc" Start a task
210
- p. done /done Complete subtask
211
- p. ship "name" /ship "name" Ship with PR
212
-
213
- TERMINAL COMMANDS (this CLI)
214
- ----------------------------
215
- prjct start First-time setup (Claude/Gemini global config)
216
- prjct init Initialize project (required for Cursor)
217
- prjct setup Reconfigure installations
218
- prjct sync Sync project state
219
- prjct watch Auto-sync on file changes (Ctrl+C to stop)
220
- prjct doctor Check system health and dependencies
221
- prjct uninstall Complete system removal of prjct
222
-
223
- FLAGS
224
- -----
225
- --quiet, -q Suppress all output (only errors to stderr)
226
- --version, -v Show version
227
- --help, -h Show this help
228
-
229
- MORE INFO
230
- ---------
231
- Documentation: https://prjct.app
232
- GitHub: https://github.com/jlopezlira/prjct-cli
233
- `)
187
+ const { getHelp } = await import('../core/utils/help')
188
+ const topic = args[1] // Optional: prjct help <command>
189
+ console.log(getHelp(topic))
234
190
  process.exitCode = 0
235
191
  } else if (args[0] === 'version' || args[0] === '-v' || args[0] === '--version') {
236
192
  // Show version with provider status
@@ -18,6 +18,7 @@ import type {
18
18
  SimpleExecutionResult,
19
19
  } from '../types'
20
20
  import { agentStream } from '../utils/agent-stream'
21
+ import { printSubtaskProgress, type SubtaskDisplay } from '../utils/subtask-table'
21
22
  import chainOfThought from './chain-of-thought'
22
23
  import contextBuilder from './context-builder'
23
24
  import groundTruth from './ground-truth'
@@ -192,7 +193,13 @@ export class CommandExecutor {
192
193
 
193
194
  // Show subtasks if fragmented
194
195
  if (orchestratorContext.requiresFragmentation && orchestratorContext.subtasks) {
195
- agentStream.status('📋', `${orchestratorContext.subtasks.length} subtasks planned`)
196
+ const subtaskDisplay: SubtaskDisplay[] = orchestratorContext.subtasks.map((s) => ({
197
+ id: s.id,
198
+ domain: s.domain,
199
+ description: s.description,
200
+ status: s.status,
201
+ }))
202
+ printSubtaskProgress(subtaskDisplay)
196
203
  }
197
204
  } catch (error) {
198
205
  // Orchestration failed - log warning but continue without it
@@ -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 {
@@ -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`)
@@ -285,6 +285,22 @@ export const COMMANDS: CommandMeta[] = [
285
285
  requiresProject: true,
286
286
  isOptional: true,
287
287
  },
288
+ {
289
+ name: 'workflow',
290
+ group: 'optional',
291
+ description: 'Configure workflow hooks via natural language',
292
+ usage: { claude: '/p:workflow ["config"]', terminal: 'prjct workflow ["config"]' },
293
+ params: '["natural language config"]',
294
+ implemented: true,
295
+ hasTemplate: true,
296
+ requiresProject: true,
297
+ isOptional: true,
298
+ features: [
299
+ 'Natural language configuration',
300
+ 'Before/after hooks for task, done, ship, sync',
301
+ 'Permanent, session, or one-time preferences',
302
+ ],
303
+ },
288
304
 
289
305
  // ===== SETUP COMMANDS =====
290
306
  {
@@ -90,6 +90,13 @@ class PrjctCommands {
90
90
  return this.workflow.resume(taskId, projectPath)
91
91
  }
92
92
 
93
+ async workflowPrefs(
94
+ input: string | null = null,
95
+ projectPath: string = process.cwd()
96
+ ): Promise<CommandResult> {
97
+ return this.workflow.workflow(input, projectPath)
98
+ }
99
+
93
100
  // ========== Planning Commands ==========
94
101
 
95
102
  async init(
@@ -213,7 +220,7 @@ class PrjctCommands {
213
220
  }
214
221
 
215
222
  showAsciiArt(): void {
216
- return this.setupCmds.showAsciiArt()
223
+ this.setupCmds.showAsciiArt()
217
224
  }
218
225
 
219
226
  // ========== Delegated Base Methods ==========
@@ -58,6 +58,7 @@ export function registerAllCommands(): void {
58
58
  commandRegistry.registerMethod('next', workflow, 'next', getMeta('next'))
59
59
  commandRegistry.registerMethod('pause', workflow, 'pause', getMeta('pause'))
60
60
  commandRegistry.registerMethod('resume', workflow, 'resume', getMeta('resume'))
61
+ commandRegistry.registerMethod('workflow', workflow, 'workflow', getMeta('workflow'))
61
62
 
62
63
  // Planning commands
63
64
  commandRegistry.registerMethod('init', planning, 'init', getMeta('init'))
@@ -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)
@@ -50,9 +50,9 @@ export class SetupCommands extends PrjctCommandsBase {
50
50
 
51
51
  if ((result.errors?.length ?? 0) > 0) {
52
52
  console.log(`\n⚠️ ${result.errors?.length ?? 0} errors:`)
53
- result.errors?.forEach((e: { file: string; error: string }) =>
53
+ for (const e of result.errors ?? []) {
54
54
  console.log(` - ${e.file}: ${e.error}`)
55
- )
55
+ }
56
56
  }
57
57
 
58
58
  console.log('\n🎉 Setup complete!')
@@ -92,9 +92,9 @@ export class SetupCommands extends PrjctCommandsBase {
92
92
 
93
93
  if ((result.errors?.length ?? 0) > 0) {
94
94
  console.log(`\n⚠️ ${result.errors?.length ?? 0} errors:`)
95
- result.errors?.forEach((e: { file: string; error: string }) =>
95
+ for (const e of result.errors ?? []) {
96
96
  console.log(` - ${e.file}: ${e.error}`)
97
- )
97
+ }
98
98
  }
99
99
 
100
100
  console.log('\n📝 Installing global configuration...')
@@ -10,6 +10,7 @@ import type { CommandResult } from '../types'
10
10
  import { isNotFoundError } from '../types/fs'
11
11
  import { showNextSteps } from '../utils/next-steps'
12
12
  import { detectProjectCommands } from '../utils/project-commands'
13
+ import { runWorkflowHooks } from '../workflow/workflow-preferences'
13
14
  import { configManager, dateHelper, fileHelper, out, PrjctCommandsBase, toolRegistry } from './base'
14
15
 
15
16
  export class ShippingCommands extends PrjctCommandsBase {
@@ -48,13 +49,20 @@ export class ShippingCommands extends PrjctCommandsBase {
48
49
  /**
49
50
  * /p:ship - Ship feature with complete automated workflow
50
51
  */
51
- async ship(feature: string | null, projectPath: string = process.cwd()): Promise<CommandResult> {
52
+ async ship(
53
+ feature: string | null,
54
+ projectPath: string = process.cwd(),
55
+ options: { skipHooks?: boolean } = {}
56
+ ): Promise<CommandResult> {
52
57
  try {
53
58
  const initResult = await this.ensureProjectInit(projectPath)
54
59
  if (!initResult.success) return initResult
55
60
 
56
- const config = await configManager.readConfig(projectPath)
57
- const projectId = config!.projectId
61
+ const projectId = await configManager.getProjectId(projectPath)
62
+ if (!projectId) {
63
+ out.fail('no project ID')
64
+ return { success: false, error: 'No project ID found' }
65
+ }
58
66
 
59
67
  let featureName = feature
60
68
  if (!featureName) {
@@ -63,6 +71,15 @@ export class ShippingCommands extends PrjctCommandsBase {
63
71
  featureName = currentTask?.description || 'current work'
64
72
  }
65
73
 
74
+ // Run before_ship hooks (using memory-based preferences)
75
+ const beforeResult = await runWorkflowHooks(projectId, 'before', 'ship', {
76
+ projectPath,
77
+ skipHooks: options.skipHooks,
78
+ })
79
+ if (!beforeResult.success) {
80
+ return { success: false, error: `Hook failed: ${beforeResult.failed}` }
81
+ }
82
+
66
83
  // Ship steps with progress indicator
67
84
  out.step(1, 5, `Linting ${featureName}...`)
68
85
  const lintResult = await this._runLint(projectPath)
@@ -108,6 +125,12 @@ export class ShippingCommands extends PrjctCommandsBase {
108
125
  })
109
126
  }
110
127
 
128
+ // Run after_ship hooks
129
+ await runWorkflowHooks(projectId, 'after', 'ship', {
130
+ projectPath,
131
+ skipHooks: options.skipHooks,
132
+ })
133
+
111
134
  out.done(`v${newVersion} shipped`)
112
135
  showNextSteps('ship')
113
136
 
@@ -16,6 +16,16 @@ import { queueStorage, stateStorage } from '../storage'
16
16
  import type { CommandResult } from '../types'
17
17
  import { showNextSteps, showStateInfo } from '../utils/next-steps'
18
18
  import { getLinearApiKey, getProjectCredentials } from '../utils/project-credentials'
19
+ import {
20
+ formatWorkflowPreferences,
21
+ type HookCommand,
22
+ type HookPhase,
23
+ listWorkflowPreferences,
24
+ type PreferenceScope,
25
+ removeWorkflowPreference,
26
+ runWorkflowHooks,
27
+ setWorkflowPreference,
28
+ } from '../workflow/workflow-preferences'
19
29
  import { configManager, dateHelper, out, PrjctCommandsBase } from './base'
20
30
 
21
31
  export class WorkflowCommands extends PrjctCommandsBase {
@@ -24,7 +34,8 @@ export class WorkflowCommands extends PrjctCommandsBase {
24
34
  */
25
35
  async now(
26
36
  task: string | null = null,
27
- projectPath: string = process.cwd()
37
+ projectPath: string = process.cwd(),
38
+ options: { skipHooks?: boolean } = {}
28
39
  ): Promise<CommandResult> {
29
40
  try {
30
41
  const initResult = await this.ensureProjectInit(projectPath)
@@ -37,6 +48,15 @@ export class WorkflowCommands extends PrjctCommandsBase {
37
48
  }
38
49
 
39
50
  if (task) {
51
+ // Run before_task hooks (using memory-based preferences)
52
+ const beforeResult = await runWorkflowHooks(projectId, 'before', 'task', {
53
+ projectPath,
54
+ skipHooks: options.skipHooks,
55
+ })
56
+ if (!beforeResult.success) {
57
+ return { success: false, error: `Hook failed: ${beforeResult.failed}` }
58
+ }
59
+
40
60
  // AGENTIC: Use CommandExecutor for full orchestration support
41
61
  const result = await commandExecutor.execute('task', { task }, projectPath)
42
62
 
@@ -97,6 +117,12 @@ export class WorkflowCommands extends PrjctCommandsBase {
97
117
  timestamp: dateHelper.getTimestamp(),
98
118
  })
99
119
 
120
+ // Run after_task hooks
121
+ await runWorkflowHooks(projectId, 'after', 'task', {
122
+ projectPath,
123
+ skipHooks: options.skipHooks,
124
+ })
125
+
100
126
  return {
101
127
  // Include full CommandExecutor result first (orchestratorContext, prompt, etc.)
102
128
  ...result,
@@ -127,7 +153,10 @@ export class WorkflowCommands extends PrjctCommandsBase {
127
153
  /**
128
154
  * /p:done - Complete current task
129
155
  */
130
- async done(projectPath: string = process.cwd()): Promise<CommandResult> {
156
+ async done(
157
+ projectPath: string = process.cwd(),
158
+ options: { skipHooks?: boolean } = {}
159
+ ): Promise<CommandResult> {
131
160
  try {
132
161
  const initResult = await this.ensureProjectInit(projectPath)
133
162
  if (!initResult.success) return initResult
@@ -146,6 +175,15 @@ export class WorkflowCommands extends PrjctCommandsBase {
146
175
  return { success: true, message: 'No active task to complete' }
147
176
  }
148
177
 
178
+ // Run before_done hooks (using memory-based preferences)
179
+ const beforeResult = await runWorkflowHooks(projectId, 'before', 'done', {
180
+ projectPath,
181
+ skipHooks: options.skipHooks,
182
+ })
183
+ if (!beforeResult.success) {
184
+ return { success: false, error: `Hook failed: ${beforeResult.failed}` }
185
+ }
186
+
149
187
  const task = currentTask.description
150
188
  let duration = ''
151
189
  if (currentTask.startedAt) {
@@ -184,6 +222,13 @@ export class WorkflowCommands extends PrjctCommandsBase {
184
222
  duration,
185
223
  timestamp: dateHelper.getTimestamp(),
186
224
  })
225
+
226
+ // Run after_done hooks
227
+ await runWorkflowHooks(projectId, 'after', 'done', {
228
+ projectPath,
229
+ skipHooks: options.skipHooks,
230
+ })
231
+
187
232
  return { success: true, task, duration }
188
233
  } catch (error) {
189
234
  out.fail((error as Error).message)
@@ -312,4 +357,62 @@ export class WorkflowCommands extends PrjctCommandsBase {
312
357
  return { success: false, error: (error as Error).message }
313
358
  }
314
359
  }
360
+
361
+ /**
362
+ * /p:workflow - View and manage workflow preferences
363
+ *
364
+ * When called without arguments, shows current preferences.
365
+ * With arguments, parses natural language and updates preferences.
366
+ */
367
+ async workflow(
368
+ input: string | null = null,
369
+ projectPath: string = process.cwd()
370
+ ): Promise<CommandResult> {
371
+ try {
372
+ const initResult = await this.ensureProjectInit(projectPath)
373
+ if (!initResult.success) return initResult
374
+
375
+ const projectId = await configManager.getProjectId(projectPath)
376
+ if (!projectId) {
377
+ out.fail('no project ID')
378
+ return { success: false, error: 'No project ID found' }
379
+ }
380
+
381
+ if (!input) {
382
+ // Show current preferences
383
+ const preferences = await listWorkflowPreferences(projectId)
384
+ console.log(formatWorkflowPreferences(preferences))
385
+ return { success: true, preferences }
386
+ }
387
+
388
+ // Return info for template-based processing
389
+ // The template/LLM will parse the natural language and call the appropriate functions
390
+ return {
391
+ success: true,
392
+ projectId,
393
+ input,
394
+ // Export functions for template use
395
+ setWorkflowPreference: async (pref: {
396
+ hook: HookPhase
397
+ command: HookCommand
398
+ action: string
399
+ scope: PreferenceScope
400
+ }) => {
401
+ await setWorkflowPreference(projectId, {
402
+ ...pref,
403
+ createdAt: dateHelper.getTimestamp(),
404
+ })
405
+ },
406
+ removeWorkflowPreference: async (hook: HookPhase, command: HookCommand) => {
407
+ await removeWorkflowPreference(projectId, hook, command)
408
+ },
409
+ listWorkflowPreferences: async () => {
410
+ return listWorkflowPreferences(projectId)
411
+ },
412
+ }
413
+ } catch (error) {
414
+ out.fail((error as Error).message)
415
+ return { success: false, error: (error as Error).message }
416
+ }
417
+ }
315
418
  }
@@ -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 {