prjct-cli 0.15.1 → 0.18.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/bin/dev.js +0 -1
  3. package/bin/serve.js +19 -20
  4. package/core/__tests__/agentic/memory-system.test.ts +2 -1
  5. package/core/__tests__/agentic/plan-mode.test.ts +2 -1
  6. package/core/agentic/agent-router.ts +79 -14
  7. package/core/agentic/command-executor/command-executor.ts +2 -74
  8. package/core/agentic/services.ts +0 -48
  9. package/core/agentic/template-loader.ts +35 -1
  10. package/core/command-registry/setup-commands.ts +15 -0
  11. package/core/commands/base.ts +96 -77
  12. package/core/commands/planning.ts +13 -2
  13. package/core/commands/setup.ts +3 -85
  14. package/core/domain/agent-generator.ts +9 -17
  15. package/core/errors.ts +209 -0
  16. package/core/infrastructure/config-manager.ts +22 -5
  17. package/core/infrastructure/path-manager.ts +23 -1
  18. package/core/infrastructure/setup.ts +5 -50
  19. package/core/storage/ideas-storage.ts +4 -0
  20. package/core/storage/queue-storage.ts +4 -0
  21. package/core/storage/shipped-storage.ts +4 -0
  22. package/core/storage/state-storage.ts +4 -0
  23. package/core/storage/storage-manager.ts +52 -13
  24. package/core/sync/auth-config.ts +145 -0
  25. package/core/sync/index.ts +30 -0
  26. package/core/sync/oauth-handler.ts +148 -0
  27. package/core/sync/sync-client.ts +252 -0
  28. package/core/sync/sync-manager.ts +358 -0
  29. package/core/utils/logger.ts +19 -12
  30. package/package.json +2 -4
  31. package/templates/agentic/subagent-generation.md +109 -0
  32. package/templates/commands/auth.md +234 -0
  33. package/templates/commands/sync.md +129 -13
  34. package/templates/subagents/domain/backend.md +105 -0
  35. package/templates/subagents/domain/database.md +118 -0
  36. package/templates/subagents/domain/devops.md +148 -0
  37. package/templates/subagents/domain/frontend.md +99 -0
  38. package/templates/subagents/domain/testing.md +169 -0
  39. package/templates/subagents/workflow/prjct-planner.md +158 -0
  40. package/templates/subagents/workflow/prjct-shipper.md +179 -0
  41. package/templates/subagents/workflow/prjct-workflow.md +98 -0
  42. package/bin/generate-views.js +0 -209
  43. package/bin/migrate-to-json.js +0 -742
  44. package/core/agentic/context-filter.ts +0 -365
  45. package/core/agentic/parallel-tools.ts +0 -165
  46. package/core/agentic/response-templates.ts +0 -164
  47. package/core/agentic/semantic-compression.ts +0 -273
  48. package/core/agentic/think-blocks.ts +0 -202
  49. package/core/agentic/validation-rules.ts +0 -313
  50. package/core/domain/agent-matcher.ts +0 -130
  51. package/core/domain/agent-validator.ts +0 -250
  52. package/core/domain/architect-session.ts +0 -315
  53. package/core/domain/product-standards.ts +0 -106
  54. package/core/domain/smart-cache.ts +0 -167
  55. package/core/domain/task-analyzer.ts +0 -296
  56. package/core/infrastructure/legacy-installer-detector/cleanup.ts +0 -216
  57. package/core/infrastructure/legacy-installer-detector/detection.ts +0 -95
  58. package/core/infrastructure/legacy-installer-detector/index.ts +0 -171
  59. package/core/infrastructure/legacy-installer-detector/migration.ts +0 -87
  60. package/core/infrastructure/legacy-installer-detector/types.ts +0 -42
  61. package/core/infrastructure/legacy-installer-detector.ts +0 -7
  62. package/core/infrastructure/migrator/file-operations.ts +0 -125
  63. package/core/infrastructure/migrator/index.ts +0 -288
  64. package/core/infrastructure/migrator/project-scanner.ts +0 -90
  65. package/core/infrastructure/migrator/reports.ts +0 -117
  66. package/core/infrastructure/migrator/types.ts +0 -124
  67. package/core/infrastructure/migrator/validation.ts +0 -94
  68. package/core/infrastructure/migrator/version-migration.ts +0 -117
  69. package/core/infrastructure/migrator.ts +0 -10
  70. package/core/infrastructure/uuid-migration.ts +0 -750
  71. package/templates/commands/migrate-all.md +0 -96
  72. package/templates/commands/migrate.md +0 -140
@@ -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()
@@ -2,24 +2,18 @@
2
2
  * Setup Module - Core installation logic
3
3
  *
4
4
  * Executes ALL setup needed for prjct-cli:
5
- * 1. Detect and clean legacy installation (curl-based)
6
- * 2. Install Claude Code CLI if missing
7
- * 3. Sync commands to ~/.claude/commands/p/
8
- * 4. Install global config ~/.claude/CLAUDE.md
9
- * 5. Migrate legacy projects automatically
10
- * 6. Save version in editors-config
5
+ * 1. Install Claude Code CLI if missing
6
+ * 2. Sync commands to ~/.claude/commands/p/
7
+ * 3. Install global config ~/.claude/CLAUDE.md
8
+ * 4. Save version in editors-config
11
9
  *
12
10
  * This module is called from:
13
11
  * - core/index.js (on first CLI use)
14
12
  * - scripts/postinstall.js (if npm scripts are enabled)
15
- *
16
- * @version 0.8.8
17
13
  */
18
14
 
19
15
  import { execSync } from 'child_process'
20
16
  import installer from './command-installer'
21
- import migrator from './migrator'
22
- import legacyDetector from './legacy-installer-detector'
23
17
  import editorsConfig from './editors-config'
24
18
  import { VERSION } from '../utils/version'
25
19
 
@@ -30,13 +24,10 @@ const DIM = '\x1b[2m'
30
24
  const NC = '\x1b[0m'
31
25
 
32
26
  interface SetupResults {
33
- legacyCleaned: boolean
34
- legacyProjectsMigrated: number
35
27
  claudeInstalled: boolean
36
28
  commandsAdded: number
37
29
  commandsUpdated: number
38
30
  configAction: string | null
39
- projectsMigrated: number
40
31
  }
41
32
 
42
33
  /**
@@ -76,21 +67,10 @@ async function installClaudeCode(): Promise<boolean> {
76
67
  */
77
68
  export async function run(): Promise<SetupResults> {
78
69
  const results: SetupResults = {
79
- legacyCleaned: false,
80
- legacyProjectsMigrated: 0,
81
70
  claudeInstalled: false,
82
71
  commandsAdded: 0,
83
72
  commandsUpdated: 0,
84
73
  configAction: null,
85
- projectsMigrated: 0,
86
- }
87
-
88
- // Step 0: Detect and clean legacy curl installation
89
- const needsLegacyCleanup = await legacyDetector.needsCleanup()
90
- if (needsLegacyCleanup) {
91
- const cleanupResult = await legacyDetector.performCleanup({ verbose: true })
92
- results.legacyCleaned = cleanupResult.success
93
- results.legacyProjectsMigrated = cleanupResult.steps.projectsMigrated
94
74
  }
95
75
 
96
76
  // Step 1: Ensure Claude Code CLI is installed
@@ -123,20 +103,9 @@ export async function run(): Promise<SetupResults> {
123
103
  if (configResult.success) {
124
104
  results.configAction = configResult.action
125
105
  }
126
-
127
- // Step 5: Migrate legacy projects automatically
128
- const migrationResult = await migrator.migrateAll({
129
- deepScan: false,
130
- cleanupLegacy: true,
131
- dryRun: false,
132
- })
133
-
134
- if (migrationResult.successfullyMigrated > 0) {
135
- results.projectsMigrated = migrationResult.successfullyMigrated
136
- }
137
106
  }
138
107
 
139
- // Step 6: Save version in editors-config
108
+ // Step 5: Save version in editors-config
140
109
  await editorsConfig.saveConfig(VERSION, installer.getInstallPath())
141
110
 
142
111
  // Show results
@@ -154,16 +123,6 @@ export default { run }
154
123
  function showResults(results: SetupResults): void {
155
124
  console.log('')
156
125
 
157
- // Show what was done
158
- if (results.legacyCleaned) {
159
- console.log(` ${GREEN}✓${NC} Legacy curl installation cleaned up`)
160
- if (results.legacyProjectsMigrated > 0) {
161
- console.log(
162
- ` ${GREEN}✓${NC} ${results.legacyProjectsMigrated} project(s) migrated from legacy`
163
- )
164
- }
165
- }
166
-
167
126
  if (results.claudeInstalled) {
168
127
  console.log(` ${GREEN}✓${NC} Claude Code CLI installed`)
169
128
  } else {
@@ -188,9 +147,5 @@ function showResults(results: SetupResults): void {
188
147
  console.log(` ${GREEN}✓${NC} Global config merged`)
189
148
  }
190
149
 
191
- if (results.projectsMigrated > 0) {
192
- console.log(` ${GREEN}✓${NC} ${results.projectsMigrated} projects migrated to global storage`)
193
- }
194
-
195
150
  console.log('')
196
151
  }
@@ -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,16 +13,46 @@ 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'
17
+
18
+ interface CacheEntry<T> {
19
+ data: T
20
+ timestamp: number
21
+ }
16
22
 
17
23
  export abstract class StorageManager<T> {
18
24
  protected filename: string
19
- protected cache: Map<string, T> = new Map()
25
+ protected cache: Map<string, CacheEntry<T>> = new Map()
20
26
  protected cacheTimeout = 5000 // 5 seconds
27
+ protected maxCacheSize = 50 // Max projects to cache
21
28
 
22
29
  constructor(filename: string) {
23
30
  this.filename = filename
24
31
  }
25
32
 
33
+ /**
34
+ * Check if cache entry is still valid
35
+ */
36
+ private isCacheValid(entry: CacheEntry<T>): boolean {
37
+ return Date.now() - entry.timestamp < this.cacheTimeout
38
+ }
39
+
40
+ /**
41
+ * Evict oldest entries if cache exceeds max size
42
+ */
43
+ private evictOldEntries(): void {
44
+ if (this.cache.size <= this.maxCacheSize) return
45
+
46
+ // Sort by timestamp and remove oldest
47
+ const entries = Array.from(this.cache.entries())
48
+ .sort((a, b) => a[1].timestamp - b[1].timestamp)
49
+
50
+ const toRemove = entries.slice(0, this.cache.size - this.maxCacheSize)
51
+ for (const [key] of toRemove) {
52
+ this.cache.delete(key)
53
+ }
54
+ }
55
+
26
56
  /**
27
57
  * Get file path for storage JSON
28
58
  */
@@ -38,17 +68,19 @@ export abstract class StorageManager<T> {
38
68
 
39
69
  /**
40
70
  * Get file path for context MD
71
+ * Uses layer-based paths to match MdBaseManager structure
41
72
  */
42
73
  protected getContextPath(projectId: string, mdFilename: string): string {
43
- return path.join(
44
- os.homedir(),
45
- '.prjct-cli/projects',
46
- projectId,
47
- 'context',
48
- mdFilename
49
- )
74
+ const layer = this.getLayer()
75
+ return pathManager.getFilePath(projectId, layer, mdFilename)
50
76
  }
51
77
 
78
+ /**
79
+ * Get the layer for context MD files
80
+ * Override in subclasses: 'core' | 'planning' | 'progress'
81
+ */
82
+ protected abstract getLayer(): string
83
+
52
84
  /**
53
85
  * Get default data structure
54
86
  */
@@ -73,10 +105,15 @@ export abstract class StorageManager<T> {
73
105
  * Read data from storage
74
106
  */
75
107
  async read(projectId: string): Promise<T> {
76
- // Check cache first
108
+ // Check cache first (with expiration)
77
109
  const cached = this.cache.get(projectId)
110
+ if (cached && this.isCacheValid(cached)) {
111
+ return cached.data
112
+ }
113
+
114
+ // Remove expired entry
78
115
  if (cached) {
79
- return cached
116
+ this.cache.delete(projectId)
80
117
  }
81
118
 
82
119
  const filePath = this.getStoragePath(projectId)
@@ -84,7 +121,8 @@ export abstract class StorageManager<T> {
84
121
  try {
85
122
  const content = await fs.readFile(filePath, 'utf-8')
86
123
  const data = JSON.parse(content) as T
87
- this.cache.set(projectId, data)
124
+ this.cache.set(projectId, { data, timestamp: Date.now() })
125
+ this.evictOldEntries()
88
126
  return data
89
127
  } catch {
90
128
  // Return default if file doesn't exist
@@ -112,8 +150,9 @@ export abstract class StorageManager<T> {
112
150
  const md = this.toMarkdown(data)
113
151
  await fs.writeFile(contextPath, md, 'utf-8')
114
152
 
115
- // 3. Update cache
116
- this.cache.set(projectId, data)
153
+ // 3. Update cache with timestamp
154
+ this.cache.set(projectId, { data, timestamp: Date.now() })
155
+ this.evictOldEntries()
117
156
 
118
157
  // 4. Publish event for backend sync (NOT included in this call - subclass handles)
119
158
  }
@@ -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'
@@ -0,0 +1,148 @@
1
+ /**
2
+ * OAuth Handler - Manages authentication flow for CLI
3
+ *
4
+ * Two modes:
5
+ * 1. Simple: User copies API key from web dashboard
6
+ * 2. Browser (future): Full OAuth device flow
7
+ */
8
+
9
+ import { authConfig } from './auth-config'
10
+ import { syncClient } from './sync-client'
11
+
12
+ export interface AuthResult {
13
+ success: boolean
14
+ email?: string
15
+ error?: string
16
+ }
17
+
18
+ class OAuthHandler {
19
+ /**
20
+ * Start authentication flow
21
+ * Opens browser to dashboard where user can create/copy API key
22
+ */
23
+ async startAuthFlow(): Promise<{ url: string }> {
24
+ const apiUrl = await authConfig.getApiUrl()
25
+ // Dashboard URL where user can get their API key
26
+ const dashboardUrl = apiUrl.replace('api.', 'app.')
27
+ const authUrl = `${dashboardUrl}/settings/api-keys`
28
+
29
+ return { url: authUrl }
30
+ }
31
+
32
+ /**
33
+ * Save API key after user provides it
34
+ */
35
+ async saveApiKey(apiKey: string): Promise<AuthResult> {
36
+ // Validate format
37
+ if (!apiKey.startsWith('prjct_')) {
38
+ return {
39
+ success: false,
40
+ error: 'Invalid API key format. Keys start with "prjct_"',
41
+ }
42
+ }
43
+
44
+ // Save temporarily to test connection
45
+ await authConfig.write({ apiKey })
46
+
47
+ // Test the key by making a request
48
+ const isValid = await syncClient.testConnection()
49
+
50
+ if (!isValid) {
51
+ // Clear the invalid key
52
+ await authConfig.clearAuth()
53
+ return {
54
+ success: false,
55
+ error: 'API key is invalid or expired',
56
+ }
57
+ }
58
+
59
+ // Key is valid - fetch user info
60
+ try {
61
+ const userInfo = await this.fetchUserInfo(apiKey)
62
+
63
+ await authConfig.saveAuth(apiKey, userInfo.id, userInfo.email)
64
+
65
+ return {
66
+ success: true,
67
+ email: userInfo.email,
68
+ }
69
+ } catch {
70
+ // Key works but couldn't fetch user info - still save it
71
+ await authConfig.write({ apiKey })
72
+ return {
73
+ success: true,
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Fetch user info using the API key
80
+ */
81
+ private async fetchUserInfo(
82
+ apiKey: string
83
+ ): Promise<{ id: string; email: string; name: string }> {
84
+ const apiUrl = await authConfig.getApiUrl()
85
+
86
+ const response = await fetch(`${apiUrl}/auth/me`, {
87
+ headers: {
88
+ 'X-Api-Key': apiKey,
89
+ },
90
+ })
91
+
92
+ if (!response.ok) {
93
+ throw new Error('Failed to fetch user info')
94
+ }
95
+
96
+ const data = (await response.json()) as {
97
+ user: { id: string; email: string; name: string }
98
+ }
99
+ return data.user
100
+ }
101
+
102
+ /**
103
+ * Check if currently authenticated
104
+ */
105
+ async isAuthenticated(): Promise<boolean> {
106
+ return await authConfig.hasAuth()
107
+ }
108
+
109
+ /**
110
+ * Get current auth status
111
+ */
112
+ async getStatus(): Promise<{
113
+ authenticated: boolean
114
+ email: string | null
115
+ apiKeyPrefix: string | null
116
+ }> {
117
+ return await authConfig.getStatus()
118
+ }
119
+
120
+ /**
121
+ * Logout - clear all auth data
122
+ */
123
+ async logout(): Promise<void> {
124
+ await authConfig.clearAuth()
125
+ }
126
+
127
+ /**
128
+ * Open URL in default browser
129
+ */
130
+ async openBrowser(url: string): Promise<void> {
131
+ const { exec } = await import('child_process')
132
+ const { promisify } = await import('util')
133
+ const execAsync = promisify(exec)
134
+
135
+ const platform = process.platform
136
+ const command =
137
+ platform === 'darwin'
138
+ ? `open "${url}"`
139
+ : platform === 'win32'
140
+ ? `start "${url}"`
141
+ : `xdg-open "${url}"`
142
+
143
+ await execAsync(command)
144
+ }
145
+ }
146
+
147
+ export const oauthHandler = new OAuthHandler()
148
+ export default oauthHandler