prjct-cli 1.6.8 → 1.6.9

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 CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.9] - 2026-02-07
4
+
5
+ ### Bug Fixes
6
+
7
+ - resolve SSE zombie connections and infinite promise leak (PRJ-286) (#134)
8
+
9
+
10
+ ## [1.6.11] - 2026-02-07
11
+
12
+ ### Bug Fixes
13
+ - **Fix SSE zombie connections and infinite promise leak (PRJ-286)**: Replaced infinite `await new Promise(() => {})` with AbortController-based mechanism that resolves on client removal. Added max client lifetime (1 hour) with per-client TTL timeout. Added periodic reaper (every 5 min) that scans for and removes zombie entries from the clients Map. Consolidated duplicate cleanup paths into single idempotent `removeClient(id)` function. Added `shutdown()` to SSEManager for clean server stop. All timers use `unref()` to avoid blocking process exit.
14
+
15
+ ### Implementation Details
16
+ Replaced the infinite pending promise in `streamSSE` callback with an `AbortController` whose signal resolves the await when the client is removed. Internal client state (`heartbeatInterval`, `ttlTimeout`, `abortController`) is tracked in an `InternalClient` wrapper separate from the public `SSEClient` type. The public `SSEClient` interface gained only `connectedAt` for staleness detection.
17
+
18
+ ### Learnings
19
+ - AbortController integrates cleanly with Hono's `streamSSE` — the async callback needs to await *something*, and a signal-based promise is the right primitive.
20
+ - Timer `unref()` has different shapes between Bun (number) and Node (Timeout object) — use `typeof` check before calling.
21
+ - Idempotent cleanup functions eliminate race conditions between heartbeat failure and stream abort handlers.
22
+
23
+ ### Test Plan
24
+
25
+ #### For QA
26
+ 1. Start prjct server, connect SSE client to `/api/events` — verify `connected` event
27
+ 2. Disconnect client gracefully — verify `clients.size === 0`
28
+ 3. Kill client process (ungraceful) — verify heartbeat cleanup within 30s
29
+ 4. Connect client, wait >1 hour — verify TTL auto-disconnect
30
+ 5. Connect 5+ clients, kill all — verify reaper cleans all within 5 min
31
+ 6. Call `server.stop()` — verify all clients disconnected and reaper stopped
32
+
33
+ #### For Users
34
+ **What changed:** SSE connections now clean up reliably on disconnect, have a 1-hour max lifetime, and a background reaper removes zombie connections every 5 minutes.
35
+ **Breaking changes:** `SSEManager` interface now includes `shutdown()`. `SSEClient` now includes `connectedAt`.
36
+
37
+ ## [1.6.10] - 2026-02-07
38
+
39
+ ### Documentation
40
+ - **Document all environment variables (PRJ-90)**: Added comprehensive environment variable documentation to README.md covering all 13 env vars used by prjct-cli. Organized into Configuration, JIRA Integration, and Agent Detection categories with defaults, descriptions, and usage examples. Added inline comments at all `process.env` read sites in 6 source files.
41
+
42
+ ### Test Plan
43
+
44
+ #### For QA
45
+ 1. `bun run build` — should succeed
46
+ 2. `bun run lint` — no errors
47
+ 3. Verify README.md renders correctly on GitHub (env vars tables)
48
+
49
+ #### For Users
50
+ **What changed:** New "Environment Variables" section in README.md with full documentation of all configurable env vars.
51
+ **Breaking changes:** None.
52
+
3
53
  ## [1.6.8] - 2026-02-07
4
54
 
5
55
  ### Refactoring
package/README.md CHANGED
@@ -132,6 +132,52 @@ prjct --version # Show version + provider status
132
132
  prjct --help # Show help
133
133
  ```
134
134
 
135
+ ## Environment Variables
136
+
137
+ ### Configuration
138
+
139
+ | Variable | Default | Description |
140
+ |----------|---------|-------------|
141
+ | `PRJCT_CLI_HOME` | `~/.prjct-cli` | Override global storage location. Useful for tests or sandboxed environments. |
142
+ | `PRJCT_DEBUG` | _(unset)_ | Enable debug logging. Values: `1`, `true`, or a log level (`error`, `warn`, `info`, `debug`). |
143
+ | `DEBUG` | _(unset)_ | Fallback debug flag (used if `PRJCT_DEBUG` is not set). Values: `1`, `true`, or `prjct`. |
144
+ | `CI` | _(unset)_ | Set automatically in CI environments. Skips interactive prompts. |
145
+
146
+ ### JIRA Integration
147
+
148
+ | Variable | Default | Description |
149
+ |----------|---------|-------------|
150
+ | `JIRA_BASE_URL` | _(none)_ | JIRA instance URL (e.g., `https://myorg.atlassian.net`). |
151
+ | `JIRA_EMAIL` | _(none)_ | Email for JIRA API authentication. |
152
+ | `JIRA_API_TOKEN` | _(none)_ | API token for JIRA authentication. Generate at [Atlassian API tokens](https://id.atlassian.com/manage-profile/security/api-tokens). |
153
+
154
+ ### Agent Detection (Auto-set)
155
+
156
+ These are typically set by the AI agent runtime, not by users:
157
+
158
+ | Variable | Description |
159
+ |----------|-------------|
160
+ | `CLAUDE_AGENT` | Set when running inside Claude Code. |
161
+ | `ANTHROPIC_CLAUDE` | Alternative Claude environment indicator. |
162
+ | `MCP_AVAILABLE` | Set when MCP (Model Context Protocol) is available. |
163
+ | `HOME` / `USERPROFILE` | Standard OS home directory (used for path resolution). |
164
+
165
+ ### Usage Examples
166
+
167
+ ```bash
168
+ # Enable debug logging
169
+ PRJCT_DEBUG=1 prjct sync
170
+
171
+ # Use a custom storage location
172
+ PRJCT_CLI_HOME=/tmp/prjct-test prjct init
173
+
174
+ # Configure JIRA integration via env vars
175
+ export JIRA_BASE_URL=https://myorg.atlassian.net
176
+ export JIRA_EMAIL=you@example.com
177
+ export JIRA_API_TOKEN=your-api-token
178
+ prjct jira setup
179
+ ```
180
+
135
181
  ## Requirements
136
182
 
137
183
  - Node.js 18+ or Bun 1.0+
@@ -13,19 +13,12 @@ import { exec } from 'node:child_process'
13
13
  import os from 'node:os'
14
14
  import path from 'node:path'
15
15
  import { promisify } from 'node:util'
16
+ import type { AIToolConfig } from '../types'
16
17
  import { fileExists } from '../utils/fs-helpers'
17
18
 
18
19
  const execAsync = promisify(exec)
19
20
 
20
- export interface AIToolConfig {
21
- id: string
22
- name: string
23
- outputFile: string
24
- outputPath: 'repo' | 'global' // 'repo' = project root, 'global' = ~/.prjct-cli/projects/{id}/context/
25
- maxTokens: number
26
- format: 'detailed' | 'concise' | 'minimal' | 'json'
27
- description: string
28
- }
21
+ export type { AIToolConfig } from '../types'
29
22
 
30
23
  /**
31
24
  * Supported AI tools registry
@@ -9,7 +9,7 @@ import commandInstaller from '../infrastructure/command-installer'
9
9
  import { generateUUID } from '../schemas'
10
10
  import type { Priority, TaskSection, TaskType } from '../schemas/state'
11
11
  import { ideasStorage, queueStorage } from '../storage'
12
- import type { CommandResult, ProjectContext } from '../types'
12
+ import type { CommandResult, InitOptions, ProjectContext } from '../types'
13
13
  import { getErrorMessage } from '../types/fs'
14
14
  import { showNextSteps } from '../utils/next-steps'
15
15
  import { OnboardingWizard } from '../wizard'
@@ -34,10 +34,7 @@ async function getAnalysisCommands(): Promise<import('./analysis').AnalysisComma
34
34
  return _analysisCommands
35
35
  }
36
36
 
37
- export interface InitOptions {
38
- yes?: boolean // Skip interactive wizard, use defaults
39
- idea?: string | null // Initial idea for architect mode
40
- }
37
+ export type { InitOptions } from '../types'
41
38
 
42
39
  export class PlanningCommands extends PrjctCommandsBase {
43
40
  /**
@@ -71,6 +68,7 @@ export class PlanningCommands extends PrjctCommandsBase {
71
68
 
72
69
  // Determine if we should run interactive wizard
73
70
  const isTTY = process.stdout.isTTY && process.stdin.isTTY
71
+ // CI: Skip interactive prompts in CI environments
74
72
  const skipWizard = opts.yes || !isTTY || process.env.CI === 'true'
75
73
 
76
74
  // Run wizard if interactive
@@ -80,10 +80,10 @@ const TERMINAL_AGENT: DetectedAgent = {
80
80
  // ============ Detection Functions ============
81
81
 
82
82
  export async function isClaudeEnvironment(): Promise<boolean> {
83
- // Environment variables
83
+ // CLAUDE_AGENT / ANTHROPIC_CLAUDE: Set by Claude runtime to indicate agent environment
84
84
  if (process.env.CLAUDE_AGENT || process.env.ANTHROPIC_CLAUDE) return true
85
85
 
86
- // MCP availability
86
+ // MCP_AVAILABLE: Set when Model Context Protocol is available
87
87
  if (global.mcp || process.env.MCP_AVAILABLE) return true
88
88
 
89
89
  // Configuration files
@@ -14,26 +14,11 @@ import fs from 'node:fs/promises'
14
14
  import os from 'node:os'
15
15
  import path from 'node:path'
16
16
  import { globSync } from 'glob'
17
- import type { SessionInfo } from '../types'
17
+ import type { MonorepoInfo, MonorepoPackage, SessionInfo } from '../types'
18
18
  import * as dateHelper from '../utils/date-helper'
19
19
  import * as fileHelper from '../utils/file-helper'
20
20
 
21
- /**
22
- * Monorepo detection result
23
- */
24
- export interface MonorepoInfo {
25
- isMonorepo: boolean
26
- type: 'pnpm' | 'npm' | 'yarn' | 'lerna' | 'nx' | 'rush' | 'turborepo' | null
27
- rootPath: string
28
- packages: MonorepoPackage[]
29
- }
30
-
31
- export interface MonorepoPackage {
32
- name: string
33
- path: string
34
- relativePath: string
35
- hasPrjctMd: boolean
36
- }
21
+ export type { MonorepoInfo, MonorepoPackage } from '../types'
37
22
 
38
23
  class PathManager {
39
24
  globalBaseDir: string
@@ -41,6 +26,7 @@ class PathManager {
41
26
  globalConfigDir: string
42
27
 
43
28
  constructor() {
29
+ // PRJCT_CLI_HOME: Override global storage location (default: ~/.prjct-cli)
44
30
  const envOverride = process.env.PRJCT_CLI_HOME?.trim()
45
31
  this.globalBaseDir = envOverride
46
32
  ? path.resolve(envOverride)
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import { getErrorMessage } from '../../types/fs'
18
+ import type { JiraAuthMode, JiraIssue, JiraProject, JiraSearchResponse } from '../../types/jira'
18
19
  import type {
19
20
  CreateIssueInput,
20
21
  FetchOptions,
@@ -27,77 +28,6 @@ import type {
27
28
  UpdateIssueInput,
28
29
  } from '../issue-tracker/types'
29
30
 
30
- // =============================================================================
31
- // JIRA API Types
32
- // =============================================================================
33
-
34
- interface JiraIssue {
35
- id: string
36
- key: string
37
- self: string
38
- fields: {
39
- summary: string
40
- description?:
41
- | {
42
- type: string
43
- content: Array<{
44
- type: string
45
- content?: Array<{ type: string; text?: string }>
46
- }>
47
- }
48
- | string
49
- | null
50
- status: {
51
- id: string
52
- name: string
53
- statusCategory: {
54
- key: string // 'new', 'indeterminate', 'done'
55
- name: string
56
- }
57
- }
58
- priority?: {
59
- id: string
60
- name: string
61
- }
62
- issuetype: {
63
- id: string
64
- name: string
65
- subtask: boolean
66
- }
67
- assignee?: {
68
- accountId: string
69
- displayName: string
70
- emailAddress?: string
71
- }
72
- reporter?: {
73
- accountId: string
74
- displayName: string
75
- emailAddress?: string
76
- }
77
- project: {
78
- id: string
79
- key: string
80
- name: string
81
- }
82
- labels: string[]
83
- created: string
84
- updated: string
85
- }
86
- }
87
-
88
- interface JiraSearchResponse {
89
- issues: JiraIssue[]
90
- total: number
91
- maxResults: number
92
- startAt: number
93
- }
94
-
95
- interface JiraProject {
96
- id: string
97
- key: string
98
- name: string
99
- }
100
-
101
31
  // =============================================================================
102
32
  // Status/Priority Mapping
103
33
  // =============================================================================
@@ -170,11 +100,7 @@ const PRIORITY_TO_JIRA: Record<IssuePriority, string> = {
170
100
  none: 'Medium',
171
101
  }
172
102
 
173
- // =============================================================================
174
- // Auth Mode Type
175
- // =============================================================================
176
-
177
- export type JiraAuthMode = 'api-token' | 'mcp' | 'none'
103
+ export type { JiraAuthMode } from '../../types/jira'
178
104
 
179
105
  // =============================================================================
180
106
  // JIRA Provider Implementation
@@ -225,7 +151,7 @@ export class JiraProvider implements IssueTrackerProvider {
225
151
  async initialize(config: JiraConfig): Promise<void> {
226
152
  this.config = config
227
153
 
228
- // Get credentials from config or environment
154
+ // JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN: JIRA API credentials (fallback from config)
229
155
  const baseUrl = config.baseUrl || process.env.JIRA_BASE_URL
230
156
  const email = process.env.JIRA_EMAIL
231
157
  const apiToken = config.apiKey || process.env.JIRA_API_TOKEN
@@ -11,15 +11,12 @@
11
11
  import { Hono } from 'hono'
12
12
  import { cors } from 'hono/cors'
13
13
  import { logger } from 'hono/logger'
14
- import type { ServerConfig, ServerInstance } from '../types'
14
+ import type { ServerConfig, ServerHandle, ServerInstance } from '../types'
15
15
  import { isBun } from '../utils/runtime'
16
16
  import { createRoutes } from './routes'
17
17
  import { createExtendedRoutes } from './routes-extended'
18
18
  import { createSSEManager } from './sse'
19
19
 
20
- // Server handle type that works for both runtimes
21
- type ServerHandle = { stop: () => void } | null
22
-
23
20
  /**
24
21
  * Create and configure the HTTP server
25
22
  */
@@ -118,6 +115,7 @@ export function createServer(config: ServerConfig): ServerInstance {
118
115
  },
119
116
 
120
117
  stop() {
118
+ sseManager.shutdown()
121
119
  if (server) {
122
120
  server.stop()
123
121
  server = null
@@ -4,18 +4,71 @@
4
4
  * Handles real-time updates to connected clients.
5
5
  * Broadcasts state changes, task updates, and notifications.
6
6
  *
7
- * @version 1.0.0
7
+ * @version 2.0.0
8
8
  */
9
9
 
10
10
  import type { Context } from 'hono'
11
11
  import { streamSSE } from 'hono/streaming'
12
- import type { SSEClient, SSEManager } from '../types'
12
+ import type { SSEClient, SSEInternalClient, SSEManager } from '../types'
13
+
14
+ /** Maximum client connection lifetime in ms (1 hour) */
15
+ const MAX_CLIENT_TTL_MS = 60 * 60 * 1000
16
+
17
+ /** Reaper interval in ms (5 minutes) */
18
+ const REAPER_INTERVAL_MS = 5 * 60 * 1000
19
+
20
+ /** Heartbeat interval in ms (30 seconds) */
21
+ const HEARTBEAT_INTERVAL_MS = 30_000
13
22
 
14
23
  /**
15
24
  * Create an SSE manager for handling real-time connections
16
25
  */
17
26
  export function createSSEManager(): SSEManager {
18
- const clients = new Map<string, SSEClient>()
27
+ const clients = new Map<string, SSEInternalClient>()
28
+ let reaperInterval: ReturnType<typeof setInterval> | null = null
29
+
30
+ /**
31
+ * Single cleanup function — all disconnect paths go through here.
32
+ * Safe to call multiple times for the same clientId.
33
+ */
34
+ function removeClient(clientId: string): void {
35
+ const entry = clients.get(clientId)
36
+ if (!entry) return
37
+
38
+ clearInterval(entry.heartbeatInterval)
39
+ clearTimeout(entry.ttlTimeout)
40
+ entry.abortController.abort()
41
+ clients.delete(clientId)
42
+ }
43
+
44
+ /** Periodic reaper that removes zombie clients */
45
+ function startReaper(): void {
46
+ if (reaperInterval) return
47
+
48
+ reaperInterval = setInterval(() => {
49
+ const now = Date.now()
50
+ for (const [id, entry] of clients) {
51
+ const connectedMs = now - new Date(entry.client.connectedAt).getTime()
52
+ if (connectedMs > MAX_CLIENT_TTL_MS) {
53
+ removeClient(id)
54
+ }
55
+ }
56
+ }, REAPER_INTERVAL_MS)
57
+
58
+ // Don't block process exit
59
+ if (reaperInterval && typeof reaperInterval === 'object' && 'unref' in reaperInterval) {
60
+ reaperInterval.unref()
61
+ }
62
+ }
63
+
64
+ function stopReaper(): void {
65
+ if (reaperInterval) {
66
+ clearInterval(reaperInterval)
67
+ reaperInterval = null
68
+ }
69
+ }
70
+
71
+ startReaper()
19
72
 
20
73
  return {
21
74
  /**
@@ -24,10 +77,13 @@ export function createSSEManager(): SSEManager {
24
77
  handleConnection(c: Context) {
25
78
  return streamSSE(c, async (stream) => {
26
79
  const clientId = crypto.randomUUID()
80
+ const connectedAt = new Date().toISOString()
81
+ const abortController = new AbortController()
27
82
 
28
83
  // Register client
29
84
  const client: SSEClient = {
30
85
  id: clientId,
86
+ connectedAt,
31
87
  send: (event, data) => {
32
88
  stream.writeSSE({
33
89
  event,
@@ -35,44 +91,61 @@ export function createSSEManager(): SSEManager {
35
91
  })
36
92
  },
37
93
  close: () => {
38
- clients.delete(clientId)
94
+ removeClient(clientId)
39
95
  },
40
96
  }
41
97
 
42
- clients.set(clientId, client)
98
+ // Heartbeat — detects dead connections
99
+ const heartbeatInterval = setInterval(async () => {
100
+ try {
101
+ await stream.writeSSE({
102
+ event: 'heartbeat',
103
+ data: JSON.stringify({ timestamp: new Date().toISOString() }),
104
+ })
105
+ } catch {
106
+ removeClient(clientId)
107
+ }
108
+ }, HEARTBEAT_INTERVAL_MS)
109
+
110
+ // TTL — force-disconnect after max lifetime
111
+ const ttlTimeout = setTimeout(() => {
112
+ removeClient(clientId)
113
+ }, MAX_CLIENT_TTL_MS)
114
+
115
+ // Don't block process exit
116
+ if (typeof heartbeatInterval === 'object' && 'unref' in heartbeatInterval) {
117
+ heartbeatInterval.unref()
118
+ }
119
+ if (typeof ttlTimeout === 'object' && 'unref' in ttlTimeout) {
120
+ ttlTimeout.unref()
121
+ }
122
+
123
+ clients.set(clientId, {
124
+ client,
125
+ heartbeatInterval,
126
+ ttlTimeout,
127
+ abortController,
128
+ } as SSEInternalClient)
43
129
 
44
130
  // Send initial connection event
45
131
  await stream.writeSSE({
46
132
  event: 'connected',
47
133
  data: JSON.stringify({
48
134
  clientId,
49
- timestamp: new Date().toISOString(),
135
+ timestamp: connectedAt,
50
136
  message: 'Connected to prjct-cli server',
51
137
  }),
52
138
  })
53
139
 
54
- // Keep connection alive with heartbeat
55
- const heartbeat = setInterval(async () => {
56
- try {
57
- await stream.writeSSE({
58
- event: 'heartbeat',
59
- data: JSON.stringify({ timestamp: new Date().toISOString() }),
60
- })
61
- } catch (_error) {
62
- // Client disconnected - expected
63
- clearInterval(heartbeat)
64
- clients.delete(clientId)
65
- }
66
- }, 30000) // Every 30 seconds
67
-
68
- // Handle disconnect
140
+ // Handle stream abort (graceful disconnect)
69
141
  stream.onAbort(() => {
70
- clearInterval(heartbeat)
71
- clients.delete(clientId)
142
+ removeClient(clientId)
72
143
  })
73
144
 
74
- // Keep stream open indefinitely
75
- await new Promise(() => {})
145
+ // Wait until abort signal fires instead of infinite promise
146
+ await new Promise<void>((resolve) => {
147
+ abortController.signal.addEventListener('abort', () => resolve(), { once: true })
148
+ })
76
149
  })
77
150
  },
78
151
 
@@ -86,12 +159,11 @@ export function createSSEManager(): SSEManager {
86
159
  timestamp: new Date().toISOString(),
87
160
  }
88
161
 
89
- for (const client of clients.values()) {
162
+ for (const [id, entry] of clients) {
90
163
  try {
91
- client.send(event, message)
92
- } catch (_error) {
93
- // Client disconnected - expected
94
- clients.delete(client.id)
164
+ entry.client.send(event, message)
165
+ } catch {
166
+ removeClient(id)
95
167
  }
96
168
  }
97
169
  },
@@ -102,35 +174,19 @@ export function createSSEManager(): SSEManager {
102
174
  getClientCount() {
103
175
  return clients.size
104
176
  },
177
+
178
+ /**
179
+ * Shut down all clients and stop the reaper.
180
+ * Called on server stop.
181
+ */
182
+ shutdown() {
183
+ stopReaper()
184
+ for (const id of [...clients.keys()]) {
185
+ removeClient(id)
186
+ }
187
+ },
105
188
  }
106
189
  }
107
190
 
108
- /**
109
- * Event types for SSE broadcasts
110
- */
111
- export const SSE_EVENTS = {
112
- // Task events
113
- TASK_STARTED: 'task:started',
114
- TASK_COMPLETED: 'task:completed',
115
- TASK_PAUSED: 'task:paused',
116
- TASK_RESUMED: 'task:resumed',
117
-
118
- // Feature events
119
- FEATURE_CREATED: 'feature:created',
120
- FEATURE_SHIPPED: 'feature:shipped',
121
-
122
- // Idea events
123
- IDEA_CAPTURED: 'idea:captured',
124
- IDEA_CONVERTED: 'idea:converted',
125
-
126
- // State events
127
- STATE_UPDATED: 'state:updated',
128
- QUEUE_UPDATED: 'queue:updated',
129
-
130
- // System events
131
- CONNECTED: 'connected',
132
- HEARTBEAT: 'heartbeat',
133
- ERROR: 'error',
134
- } as const
135
-
136
- export type SSEEventType = (typeof SSE_EVENTS)[keyof typeof SSE_EVENTS]
191
+ export type { SSEEventType } from '../types/server'
192
+ export { SSE_EVENTS } from '../types/server'
@@ -12,50 +12,25 @@
12
12
  import fs from 'node:fs/promises'
13
13
  import path from 'node:path'
14
14
  import pathManager from '../infrastructure/path-manager'
15
+ import type {
16
+ ContextGeneratorConfig,
17
+ GitData,
18
+ ProjectCommands,
19
+ ProjectStats,
20
+ SyncAgentInfo,
21
+ } from '../types'
15
22
  import { type ContextSources, cite, defaultSources } from '../utils/citations'
16
23
  import * as dateHelper from '../utils/date-helper'
17
24
  import { mergePreservedSections, validatePreserveBlocks } from '../utils/preserve-sections'
18
25
  import { NestedContextResolver } from './nested-context-resolver'
19
26
 
20
- // ============================================================================
21
- // TYPES
22
- // ============================================================================
23
-
24
- export interface GitData {
25
- branch: string
26
- commits: number
27
- }
28
-
29
- export interface ProjectStats {
30
- name: string
31
- version: string
32
- ecosystem: string
33
- projectType: string
34
- fileCount: number
35
- languages: string[]
36
- frameworks: string[]
37
- }
38
-
39
- export interface Commands {
40
- install: string
41
- dev: string
42
- test: string
43
- build: string
44
- lint: string
45
- format: string
46
- }
47
-
48
- export interface AgentInfo {
49
- name: string
50
- type: 'workflow' | 'domain'
51
- skill?: string
52
- }
53
-
54
- export interface ContextGeneratorConfig {
55
- projectId: string
56
- projectPath: string
57
- globalPath: string
58
- }
27
+ export type {
28
+ ContextGeneratorConfig,
29
+ GitData,
30
+ ProjectCommands,
31
+ ProjectStats,
32
+ SyncAgentInfo,
33
+ } from '../types'
59
34
 
60
35
  // ============================================================================
61
36
  // CONTEXT FILE GENERATOR
@@ -103,8 +78,8 @@ export class ContextFileGenerator {
103
78
  async generate(
104
79
  git: GitData,
105
80
  stats: ProjectStats,
106
- commands: Commands,
107
- agents: AgentInfo[],
81
+ commands: ProjectCommands,
82
+ agents: SyncAgentInfo[],
108
83
  sources?: ContextSources
109
84
  ): Promise<string[]> {
110
85
  const contextPath = path.join(this.config.globalPath, 'context')
@@ -138,8 +113,8 @@ export class ContextFileGenerator {
138
113
  contextPath: string,
139
114
  git: GitData,
140
115
  stats: ProjectStats,
141
- commands: Commands,
142
- agents: AgentInfo[],
116
+ commands: ProjectCommands,
117
+ agents: SyncAgentInfo[],
143
118
  sources?: ContextSources
144
119
  ): Promise<void> {
145
120
  const workflowAgents = agents.filter((a) => a.type === 'workflow').map((a) => a.name)
@@ -348,8 +323,8 @@ ${
348
323
  async generateMonorepoContexts(
349
324
  git: GitData,
350
325
  stats: ProjectStats,
351
- commands: Commands,
352
- agents: AgentInfo[]
326
+ commands: ProjectCommands,
327
+ agents: SyncAgentInfo[]
353
328
  ): Promise<string[]> {
354
329
  const monoInfo = await pathManager.detectMonorepo(this.config.projectPath)
355
330
 
@@ -394,8 +369,8 @@ ${
394
369
  resolvedCtx: { content: string; sources: string[]; overrides: string[] },
395
370
  git: GitData,
396
371
  stats: ProjectStats,
397
- commands: Commands,
398
- agents: AgentInfo[]
372
+ commands: ProjectCommands,
373
+ agents: SyncAgentInfo[]
399
374
  ): Promise<string> {
400
375
  const workflowAgents = agents.filter((a) => a.type === 'workflow').map((a) => a.name)
401
376
  const domainAgents = agents.filter((a) => a.type === 'domain').map((a) => a.name)