prjct-cli 0.17.0 → 0.18.1

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.
@@ -29,7 +29,8 @@ describe('MemorySystem P3.3', () => {
29
29
  userTriggered: true
30
30
  })
31
31
 
32
- expect(memoryId).toMatch(/^mem_/)
32
+ // UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
33
+ expect(memoryId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)
33
34
 
34
35
  const memories = await memorySystem.getAllMemories(TEST_PROJECT_ID)
35
36
  expect(memories.length).toBe(1)
@@ -92,7 +92,8 @@ describe('PlanMode P3.4', () => {
92
92
  it('should create a new plan with correct initial state', () => {
93
93
  const plan = planMode.startPlanning(TEST_PROJECT_ID, 'feature', { description: 'Add dark mode' })
94
94
 
95
- expect(plan.id).toMatch(/^plan_/)
95
+ // UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
96
+ expect(plan.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)
96
97
  expect(plan.projectId).toBe(TEST_PROJECT_ID)
97
98
  expect(plan.command).toBe('feature')
98
99
  expect(plan.status).toBe(PLAN_STATUS.GATHERING)
@@ -2,14 +2,13 @@
2
2
  * Agent Router
3
3
  * Orchestrates agent loading and context building for Claude delegation.
4
4
  *
5
- * Loads agents from two locations:
6
- * 1. {projectPath}/.claude/agents/ - Claude Code sub-agents (PRIMARY, per-project)
7
- * 2. ~/.prjct-cli/projects/{id}/agents/ - Legacy agents (fallback)
5
+ * Loads agents from global storage:
6
+ * ~/.prjct-cli/projects/{id}/agents/
8
7
  *
9
- * Claude Code sub-agents are prioritized as they're more specific to the project.
8
+ * Agents are dynamically generated by /p:sync based on project tech stack.
10
9
  *
11
10
  * @module agentic/agent-router
12
- * @version 3.0.0
11
+ * @version 4.0.0
13
12
  */
14
13
 
15
14
  import fs from 'fs/promises'
@@ -20,7 +19,6 @@ import pathManager from '../infrastructure/path-manager'
20
19
  interface Agent {
21
20
  name: string
22
21
  content: string
23
- source: 'claude-code' | 'legacy'
24
22
  }
25
23
 
26
24
  interface AssignmentContext {
@@ -38,15 +36,7 @@ interface AssignmentContext {
38
36
  class AgentRouter {
39
37
  projectId: string | null = null
40
38
  projectPath: string | null = null
41
- legacyAgentsPath: string | null = null
42
-
43
- /**
44
- * Get path to Claude Code sub-agents directory
45
- */
46
- getClaudeCodeAgentsPath(): string | null {
47
- if (!this.projectPath) return null
48
- return path.join(this.projectPath, '.claude', 'agents')
49
- }
39
+ agentsPath: string | null = null
50
40
 
51
41
  /**
52
42
  * Initialize router with project context
@@ -54,25 +44,24 @@ class AgentRouter {
54
44
  async initialize(projectPath: string): Promise<void> {
55
45
  this.projectId = await configManager.getProjectId(projectPath)
56
46
  this.projectPath = projectPath
57
- this.legacyAgentsPath = pathManager.getPath(this.projectId!, 'agents')
47
+ this.agentsPath = pathManager.getFilePath(this.projectId!, 'agents', '')
58
48
  }
59
49
 
60
50
  /**
61
- * Load agents from a specific directory
51
+ * Load all available agents from global storage
62
52
  */
63
- private async loadAgentsFromPath(
64
- agentsPath: string,
65
- source: 'claude-code' | 'legacy'
66
- ): Promise<Agent[]> {
53
+ async loadAvailableAgents(): Promise<Agent[]> {
54
+ if (!this.agentsPath) return []
55
+
67
56
  try {
68
- const files = await fs.readdir(agentsPath)
57
+ const files = await fs.readdir(this.agentsPath)
69
58
  const agents: Agent[] = []
70
59
 
71
60
  for (const file of files) {
72
61
  if (file.endsWith('.md')) {
73
62
  const name = file.replace('.md', '')
74
- const content = await fs.readFile(path.join(agentsPath, file), 'utf-8')
75
- agents.push({ name, content, source })
63
+ const content = await fs.readFile(path.join(this.agentsPath, file), 'utf-8')
64
+ agents.push({ name, content })
76
65
  }
77
66
  }
78
67
 
@@ -82,33 +71,6 @@ class AgentRouter {
82
71
  }
83
72
  }
84
73
 
85
- /**
86
- * Load all available agents from both Claude Code and legacy locations
87
- * Claude Code sub-agents take priority over legacy agents with same name
88
- */
89
- async loadAvailableAgents(): Promise<Agent[]> {
90
- const agentMap = new Map<string, Agent>()
91
-
92
- // Load legacy agents first (lower priority)
93
- if (this.legacyAgentsPath) {
94
- const legacyAgents = await this.loadAgentsFromPath(this.legacyAgentsPath, 'legacy')
95
- for (const agent of legacyAgents) {
96
- agentMap.set(agent.name, agent)
97
- }
98
- }
99
-
100
- // Load Claude Code sub-agents (higher priority - overwrites legacy)
101
- const claudeCodePath = this.getClaudeCodeAgentsPath()
102
- if (claudeCodePath) {
103
- const claudeCodeAgents = await this.loadAgentsFromPath(claudeCodePath, 'claude-code')
104
- for (const agent of claudeCodeAgents) {
105
- agentMap.set(agent.name, agent)
106
- }
107
- }
108
-
109
- return Array.from(agentMap.values())
110
- }
111
-
112
74
  /**
113
75
  * Get list of available agent names
114
76
  */
@@ -118,34 +80,18 @@ class AgentRouter {
118
80
  }
119
81
 
120
82
  /**
121
- * Load a specific agent by name
122
- * Checks Claude Code sub-agents first, then falls back to legacy
83
+ * Load a specific agent by name from global storage
123
84
  */
124
85
  async loadAgent(name: string): Promise<Agent | null> {
125
- // Try Claude Code sub-agents first (higher priority)
126
- const claudeCodePath = this.getClaudeCodeAgentsPath()
127
- if (claudeCodePath) {
128
- try {
129
- const filePath = path.join(claudeCodePath, `${name}.md`)
130
- const content = await fs.readFile(filePath, 'utf-8')
131
- return { name, content, source: 'claude-code' }
132
- } catch {
133
- // Not found in Claude Code path, try legacy
134
- }
135
- }
86
+ if (!this.agentsPath) return null
136
87
 
137
- // Fall back to legacy agents
138
- if (this.legacyAgentsPath) {
139
- try {
140
- const filePath = path.join(this.legacyAgentsPath, `${name}.md`)
141
- const content = await fs.readFile(filePath, 'utf-8')
142
- return { name, content, source: 'legacy' }
143
- } catch {
144
- // Not found
145
- }
88
+ try {
89
+ const filePath = path.join(this.agentsPath, `${name}.md`)
90
+ const content = await fs.readFile(filePath, 'utf-8')
91
+ return { name, content }
92
+ } catch {
93
+ return null
146
94
  }
147
-
148
- return null
149
95
  }
150
96
 
151
97
  /**
@@ -65,4 +65,19 @@ export const SETUP_COMMANDS: Command[] = [
65
65
  requiresInit: false,
66
66
  blockingRules: null,
67
67
  },
68
+
69
+ {
70
+ name: 'auth',
71
+ category: 'setup',
72
+ description: 'Manage cloud authentication',
73
+ usage: {
74
+ claude: '/p:auth [login|logout|status]',
75
+ terminal: 'prjct auth [login|logout|status]',
76
+ },
77
+ params: '[login|logout|status]',
78
+ implemented: true,
79
+ hasTemplate: true,
80
+ requiresInit: false,
81
+ blockingRules: null,
82
+ },
68
83
  ]
@@ -38,9 +38,9 @@ class AgentGenerator {
38
38
 
39
39
  constructor(projectId: string | null = null) {
40
40
  this.projectId = projectId
41
- // NEW: Write to data/agents/ for JSON storage (OpenCode-style)
41
+ // Write to agents/ for MD storage (matches AgentLoader)
42
42
  this.outputDir = projectId
43
- ? path.join(os.homedir(), '.prjct-cli', 'projects', projectId, 'data', 'agents')
43
+ ? path.join(os.homedir(), '.prjct-cli', 'projects', projectId, 'agents')
44
44
  : path.join(os.homedir(), '.prjct-cli', 'agents')
45
45
  this.loader = new AgentLoader(projectId)
46
46
  }
@@ -54,18 +54,10 @@ class AgentGenerator {
54
54
  log.debug(`Generating ${agentName} agent...`)
55
55
  await fs.mkdir(this.outputDir, { recursive: true })
56
56
 
57
- // Write as JSON (OpenCode-style storage)
58
- const agent = {
59
- name: agentName,
60
- role: config.role || agentName,
61
- domain: config.domain || 'general',
62
- expertise: config.expertise || '',
63
- contextFilter: config.contextFilter || 'Only relevant files',
64
- createdAt: new Date().toISOString()
65
- }
66
-
67
- const outputPath = path.join(this.outputDir, `${agentName}.json`)
68
- await fs.writeFile(outputPath, JSON.stringify(agent, null, 2), 'utf-8')
57
+ // Write as MD (matches AgentLoader which reads .md files)
58
+ const content = this.buildAgentPrompt(agentName, config)
59
+ const outputPath = path.join(this.outputDir, `${agentName}.md`)
60
+ await fs.writeFile(outputPath, content, 'utf-8')
69
61
  log.debug(`${agentName} agent created`)
70
62
 
71
63
  return { name: agentName }
@@ -135,10 +127,10 @@ ${config.contextFilter || 'Only relevant files'}
135
127
 
136
128
  try {
137
129
  const files = await fs.readdir(this.outputDir)
138
- const agentFiles = files.filter((f) => f.endsWith('.json') && !f.startsWith('.'))
130
+ const agentFiles = files.filter((f) => f.endsWith('.md') && !f.startsWith('.'))
139
131
 
140
132
  for (const file of agentFiles) {
141
- const type = file.replace('.json', '')
133
+ const type = file.replace('.md', '')
142
134
 
143
135
  if (!requiredAgents.includes(type)) {
144
136
  const filePath = path.join(this.outputDir, file)
@@ -160,7 +152,7 @@ ${config.contextFilter || 'Only relevant files'}
160
152
  async listAgents(): Promise<string[]> {
161
153
  try {
162
154
  const files = await fs.readdir(this.outputDir)
163
- return files.filter((f) => f.endsWith('.json') && !f.startsWith('.')).map((f) => f.replace('.json', ''))
155
+ return files.filter((f) => f.endsWith('.md') && !f.startsWith('.')).map((f) => f.replace('.md', ''))
164
156
  } catch {
165
157
  return []
166
158
  }
@@ -114,7 +114,7 @@ class PathManager {
114
114
 
115
115
  const projectPath = this.getGlobalProjectPath(projectId)
116
116
 
117
- const layers = ['core', 'progress', 'planning', 'analysis', 'memory']
117
+ const layers = ['core', 'progress', 'planning', 'analysis', 'memory', 'agents']
118
118
 
119
119
  for (const layer of layers) {
120
120
  await fileHelper.ensureDir(path.join(projectPath, layer))
@@ -252,6 +252,28 @@ class PathManager {
252
252
  }
253
253
  return absolutePath
254
254
  }
255
+
256
+ /**
257
+ * Get the auth config file path for cloud sync
258
+ * Stored in global config directory, not project-specific
259
+ */
260
+ getAuthConfigPath(): string {
261
+ return path.join(this.globalConfigDir, 'auth.json')
262
+ }
263
+
264
+ /**
265
+ * Get the sync pending events file path for a project
266
+ */
267
+ getSyncPendingPath(projectId: string): string {
268
+ return path.join(this.getGlobalProjectPath(projectId), 'sync', 'pending.json')
269
+ }
270
+
271
+ /**
272
+ * Get the last sync timestamp file path for a project
273
+ */
274
+ getLastSyncPath(projectId: string): string {
275
+ return path.join(this.getGlobalProjectPath(projectId), 'sync', 'last-sync.json')
276
+ }
255
277
  }
256
278
 
257
279
  const pathManager = new PathManager()
@@ -42,6 +42,10 @@ class IdeasStorage extends StorageManager<IdeasJson> {
42
42
  return 'ideas.md'
43
43
  }
44
44
 
45
+ protected getLayer(): string {
46
+ return 'planning'
47
+ }
48
+
45
49
  protected getEventType(action: 'update' | 'create' | 'delete'): string {
46
50
  return `ideas.${action}d`
47
51
  }
@@ -25,6 +25,10 @@ class QueueStorage extends StorageManager<QueueJson> {
25
25
  return 'next.md'
26
26
  }
27
27
 
28
+ protected getLayer(): string {
29
+ return 'core'
30
+ }
31
+
28
32
  protected getEventType(action: 'update' | 'create' | 'delete'): string {
29
33
  return `queue.${action}d`
30
34
  }
@@ -39,6 +39,10 @@ class ShippedStorage extends StorageManager<ShippedJson> {
39
39
  return 'shipped.md'
40
40
  }
41
41
 
42
+ protected getLayer(): string {
43
+ return 'progress'
44
+ }
45
+
42
46
  protected getEventType(action: 'update' | 'create' | 'delete'): string {
43
47
  return `shipped.${action}d`
44
48
  }
@@ -26,6 +26,10 @@ class StateStorage extends StorageManager<StateJson> {
26
26
  return 'now.md'
27
27
  }
28
28
 
29
+ protected getLayer(): string {
30
+ return 'core'
31
+ }
32
+
29
33
  protected getEventType(action: 'update' | 'create' | 'delete'): string {
30
34
  return `state.${action}d`
31
35
  }
@@ -13,6 +13,7 @@ import fs from 'fs/promises'
13
13
  import path from 'path'
14
14
  import os from 'os'
15
15
  import { eventBus, type SyncEvent } from '../events'
16
+ import pathManager from '../infrastructure/path-manager'
16
17
 
17
18
  interface CacheEntry<T> {
18
19
  data: T
@@ -67,17 +68,19 @@ export abstract class StorageManager<T> {
67
68
 
68
69
  /**
69
70
  * Get file path for context MD
71
+ * Uses layer-based paths to match MdBaseManager structure
70
72
  */
71
73
  protected getContextPath(projectId: string, mdFilename: string): string {
72
- return path.join(
73
- os.homedir(),
74
- '.prjct-cli/projects',
75
- projectId,
76
- 'context',
77
- mdFilename
78
- )
74
+ const layer = this.getLayer()
75
+ return pathManager.getFilePath(projectId, layer, mdFilename)
79
76
  }
80
77
 
78
+ /**
79
+ * Get the layer for context MD files
80
+ * Override in subclasses: 'core' | 'planning' | 'progress'
81
+ */
82
+ protected abstract getLayer(): string
83
+
81
84
  /**
82
85
  * Get default data structure
83
86
  */
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Auth Config - Manages API key storage for cloud sync
3
+ *
4
+ * Stores credentials in ~/.prjct-cli/config/auth.json
5
+ * Used by SyncClient to authenticate with prjct API
6
+ */
7
+
8
+ import path from 'path'
9
+ import os from 'os'
10
+ import * as fileHelper from '../utils/file-helper'
11
+
12
+ export interface AuthConfig {
13
+ apiKey: string | null
14
+ apiUrl: string
15
+ userId: string | null
16
+ email: string | null
17
+ lastAuth: string | null
18
+ }
19
+
20
+ const DEFAULT_API_URL = 'https://api.prjct.app'
21
+
22
+ const DEFAULT_CONFIG: AuthConfig = {
23
+ apiKey: null,
24
+ apiUrl: DEFAULT_API_URL,
25
+ userId: null,
26
+ email: null,
27
+ lastAuth: null,
28
+ }
29
+
30
+ class AuthConfigManager {
31
+ private configPath: string
32
+ private cachedConfig: AuthConfig | null = null
33
+
34
+ constructor() {
35
+ this.configPath = path.join(os.homedir(), '.prjct-cli', 'config', 'auth.json')
36
+ }
37
+
38
+ /**
39
+ * Get the auth config file path
40
+ */
41
+ getConfigPath(): string {
42
+ return this.configPath
43
+ }
44
+
45
+ /**
46
+ * Read auth config from disk
47
+ */
48
+ async read(): Promise<AuthConfig> {
49
+ if (this.cachedConfig) {
50
+ return this.cachedConfig
51
+ }
52
+
53
+ const config = await fileHelper.readJson<AuthConfig>(this.configPath)
54
+ this.cachedConfig = config ?? { ...DEFAULT_CONFIG }
55
+ return this.cachedConfig
56
+ }
57
+
58
+ /**
59
+ * Write auth config to disk
60
+ */
61
+ async write(config: Partial<AuthConfig>): Promise<void> {
62
+ const current = await this.read()
63
+ const updated: AuthConfig = {
64
+ ...current,
65
+ ...config,
66
+ lastAuth: new Date().toISOString(),
67
+ }
68
+
69
+ await fileHelper.ensureDir(path.dirname(this.configPath))
70
+ await fileHelper.writeJson(this.configPath, updated)
71
+ this.cachedConfig = updated
72
+ }
73
+
74
+ /**
75
+ * Check if user is authenticated (has valid API key)
76
+ */
77
+ async hasAuth(): Promise<boolean> {
78
+ const config = await this.read()
79
+ return config.apiKey !== null && config.apiKey.length > 0
80
+ }
81
+
82
+ /**
83
+ * Get the API key if available
84
+ */
85
+ async getApiKey(): Promise<string | null> {
86
+ const config = await this.read()
87
+ return config.apiKey
88
+ }
89
+
90
+ /**
91
+ * Get the API URL (allows override for dev/staging)
92
+ */
93
+ async getApiUrl(): Promise<string> {
94
+ const config = await this.read()
95
+ return config.apiUrl || DEFAULT_API_URL
96
+ }
97
+
98
+ /**
99
+ * Save API key and user info after successful auth
100
+ */
101
+ async saveAuth(apiKey: string, userId: string, email: string): Promise<void> {
102
+ await this.write({
103
+ apiKey,
104
+ userId,
105
+ email,
106
+ })
107
+ }
108
+
109
+ /**
110
+ * Clear all auth data (logout)
111
+ */
112
+ async clearAuth(): Promise<void> {
113
+ this.cachedConfig = { ...DEFAULT_CONFIG }
114
+ await fileHelper.writeJson(this.configPath, this.cachedConfig)
115
+ }
116
+
117
+ /**
118
+ * Get auth status for display
119
+ */
120
+ async getStatus(): Promise<{
121
+ authenticated: boolean
122
+ email: string | null
123
+ apiKeyPrefix: string | null
124
+ lastAuth: string | null
125
+ }> {
126
+ const config = await this.read()
127
+
128
+ return {
129
+ authenticated: config.apiKey !== null,
130
+ email: config.email,
131
+ apiKeyPrefix: config.apiKey ? config.apiKey.substring(0, 12) + '...' : null,
132
+ lastAuth: config.lastAuth,
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Clear cache (useful for testing)
138
+ */
139
+ clearCache(): void {
140
+ this.cachedConfig = null
141
+ }
142
+ }
143
+
144
+ export const authConfig = new AuthConfigManager()
145
+ export default authConfig
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Sync Module - Cloud synchronization for prjct-cli
3
+ *
4
+ * Provides:
5
+ * - AuthConfig: API key storage and management
6
+ * - SyncClient: HTTP client for prjct API
7
+ * - SyncManager: Orchestrates push/pull operations
8
+ * - OAuthHandler: Authentication flow management
9
+ */
10
+
11
+ // Auth
12
+ export { authConfig, type AuthConfig } from './auth-config'
13
+
14
+ // OAuth
15
+ export { oauthHandler, type AuthResult } from './oauth-handler'
16
+
17
+ // Client
18
+ export {
19
+ syncClient,
20
+ type SyncBatchResult,
21
+ type SyncPullResult,
22
+ type SyncStatus,
23
+ type SyncClientError,
24
+ } from './sync-client'
25
+
26
+ // Manager
27
+ export { syncManager, type SyncResult, type PushResult, type PullResult } from './sync-manager'
28
+
29
+ // Default export is the main sync manager
30
+ export { syncManager as default } from './sync-manager'