@proletariat/cli 0.3.51 → 0.3.52

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 (87) hide show
  1. package/dist/commands/agent/status.js +1 -0
  2. package/dist/commands/asana/connect.d.ts +15 -0
  3. package/dist/commands/asana/connect.js +267 -0
  4. package/dist/commands/asana/sync.d.ts +15 -0
  5. package/dist/commands/asana/sync.js +189 -0
  6. package/dist/commands/config/index.js +7 -1
  7. package/dist/commands/execution/list.js +3 -0
  8. package/dist/commands/execution/view.js +10 -0
  9. package/dist/commands/monday/connect.d.ts +16 -0
  10. package/dist/commands/monday/connect.js +212 -0
  11. package/dist/commands/monday/sync.d.ts +14 -0
  12. package/dist/commands/monday/sync.js +178 -0
  13. package/dist/commands/orchestrator/start.d.ts +6 -0
  14. package/dist/commands/orchestrator/start.js +149 -11
  15. package/dist/commands/session/list.js +6 -5
  16. package/dist/commands/work/index.js +7 -0
  17. package/dist/commands/work/jira.d.ts +28 -0
  18. package/dist/commands/work/jira.js +225 -0
  19. package/dist/commands/work/source/set.d.ts +12 -0
  20. package/dist/commands/work/source/set.js +52 -0
  21. package/dist/commands/work/source.d.ts +11 -0
  22. package/dist/commands/work/source.js +53 -0
  23. package/dist/commands/work/spawn.d.ts +1 -0
  24. package/dist/commands/work/spawn.js +73 -8
  25. package/dist/commands/work/start.d.ts +8 -0
  26. package/dist/commands/work/start.js +241 -3
  27. package/dist/lib/asana/client.d.ts +15 -0
  28. package/dist/lib/asana/client.js +120 -0
  29. package/dist/lib/asana/config.d.ts +9 -0
  30. package/dist/lib/asana/config.js +61 -0
  31. package/dist/lib/asana/index.d.ts +5 -0
  32. package/dist/lib/asana/index.js +4 -0
  33. package/dist/lib/asana/mapper.d.ts +13 -0
  34. package/dist/lib/asana/mapper.js +70 -0
  35. package/dist/lib/asana/sync.d.ts +13 -0
  36. package/dist/lib/asana/sync.js +36 -0
  37. package/dist/lib/asana/types.d.ts +40 -0
  38. package/dist/lib/asana/types.js +1 -0
  39. package/dist/lib/database/drizzle-schema.d.ts +393 -0
  40. package/dist/lib/database/drizzle-schema.js +45 -0
  41. package/dist/lib/execution/config.d.ts +10 -0
  42. package/dist/lib/execution/config.js +19 -0
  43. package/dist/lib/execution/runners.d.ts +10 -0
  44. package/dist/lib/execution/runners.js +110 -1
  45. package/dist/lib/execution/spawner.js +26 -0
  46. package/dist/lib/execution/storage.d.ts +4 -0
  47. package/dist/lib/execution/storage.js +8 -3
  48. package/dist/lib/execution/types.d.ts +4 -0
  49. package/dist/lib/external-issues/adapters.d.ts +18 -1
  50. package/dist/lib/external-issues/adapters.js +49 -1
  51. package/dist/lib/external-issues/index.d.ts +4 -1
  52. package/dist/lib/external-issues/index.js +5 -0
  53. package/dist/lib/external-issues/jira.d.ts +23 -0
  54. package/dist/lib/external-issues/jira.js +223 -0
  55. package/dist/lib/external-issues/linear.js +4 -3
  56. package/dist/lib/external-issues/mapper.d.ts +3 -2
  57. package/dist/lib/external-issues/mapper.js +5 -2
  58. package/dist/lib/external-issues/mapping-store.d.ts +12 -0
  59. package/dist/lib/external-issues/mapping-store.js +164 -0
  60. package/dist/lib/external-issues/types.d.ts +34 -0
  61. package/dist/lib/external-issues/validation.js +11 -0
  62. package/dist/lib/external-issues/work-start.d.ts +10 -0
  63. package/dist/lib/external-issues/work-start.js +12 -0
  64. package/dist/lib/linear/mapper.d.ts +2 -0
  65. package/dist/lib/linear/mapper.js +66 -2
  66. package/dist/lib/monday/client.d.ts +14 -0
  67. package/dist/lib/monday/client.js +113 -0
  68. package/dist/lib/monday/config.d.ts +10 -0
  69. package/dist/lib/monday/config.js +64 -0
  70. package/dist/lib/monday/index.d.ts +5 -0
  71. package/dist/lib/monday/index.js +4 -0
  72. package/dist/lib/monday/mapper.d.ts +14 -0
  73. package/dist/lib/monday/mapper.js +89 -0
  74. package/dist/lib/monday/sync.d.ts +13 -0
  75. package/dist/lib/monday/sync.js +45 -0
  76. package/dist/lib/monday/types.d.ts +38 -0
  77. package/dist/lib/monday/types.js +4 -0
  78. package/dist/lib/pmo/schema.d.ts +10 -1
  79. package/dist/lib/pmo/schema.js +73 -0
  80. package/dist/lib/pmo/storage/base.js +32 -0
  81. package/dist/lib/prompt-json.d.ts +11 -0
  82. package/dist/lib/work-source/config.d.ts +14 -0
  83. package/dist/lib/work-source/config.js +70 -0
  84. package/dist/lib/work-source/index.d.ts +1 -0
  85. package/dist/lib/work-source/index.js +1 -0
  86. package/oclif.manifest.json +2584 -2017
  87. package/package.json +1 -1
@@ -152,6 +152,107 @@ export function getDockerCredentialInfo() {
152
152
  return null;
153
153
  }
154
154
  }
155
+ /**
156
+ * Check if Claude Code authentication is available on the host system.
157
+ * Returns true if either:
158
+ * 1. OAuth credentials exist in ~/.claude/.credentials.json, OR
159
+ * 2. ANTHROPIC_API_KEY environment variable is set
160
+ *
161
+ * This is used to validate auth before spawning host sessions (e.g., orchestrator)
162
+ * to avoid creating stuck sessions when the keychain is locked (SSH contexts).
163
+ */
164
+ export function hostCredentialsExist() {
165
+ // Check for ANTHROPIC_API_KEY first (works in all contexts, including SSH)
166
+ if (process.env.ANTHROPIC_API_KEY) {
167
+ return true;
168
+ }
169
+ // Check for OAuth credentials in ~/.claude/.credentials.json
170
+ try {
171
+ const homeDir = process.env.HOME || os.homedir();
172
+ const credPath = path.join(homeDir, '.claude', '.credentials.json');
173
+ if (!fs.existsSync(credPath)) {
174
+ return false;
175
+ }
176
+ const credData = fs.readFileSync(credPath, 'utf-8');
177
+ const creds = JSON.parse(credData);
178
+ // Check if OAuth credentials exist (similar to Docker check)
179
+ // Don't check expiration - Claude Code handles token refresh internally
180
+ if (creds.claudeAiOauth?.accessToken) {
181
+ return true;
182
+ }
183
+ return false;
184
+ }
185
+ catch {
186
+ return false;
187
+ }
188
+ }
189
+ /**
190
+ * Ensure tmux server has keychain access for Claude Code OAuth.
191
+ *
192
+ * On macOS, tmux sessions can lose access to the keychain if the tmux server
193
+ * was started in a context without keychain access (e.g., from a background
194
+ * process, SSH session, or parent process with restricted keychain access).
195
+ *
196
+ * This function:
197
+ * 1. Checks if a tmux server is running
198
+ * 2. Tests if it can access Claude Code OAuth credentials
199
+ * 3. If not, restarts the tmux server to restore keychain access
200
+ *
201
+ * This runs transparently before spawning agent sessions, ensuring OAuth
202
+ * authentication works without manual intervention.
203
+ */
204
+ async function ensureTmuxServerHasKeychainAccess() {
205
+ // Skip if no tmux server is running (will be started fresh with keychain access)
206
+ try {
207
+ const serverRunning = execSync('tmux list-sessions 2>/dev/null || echo ""', {
208
+ encoding: 'utf-8',
209
+ stdio: 'pipe'
210
+ });
211
+ if (!serverRunning.trim()) {
212
+ return; // No server running, will start fresh
213
+ }
214
+ }
215
+ catch {
216
+ return; // tmux not installed or no server running
217
+ }
218
+ // Test if tmux server can access Claude Code credentials
219
+ // We spawn a test session and check if Claude Code can authenticate
220
+ const testSession = `prlt-keychain-test-${Date.now()}`;
221
+ try {
222
+ // Create test session
223
+ execSync(`tmux new-session -d -s "${testSession}"`, { stdio: 'pipe' });
224
+ // Send command to check Claude Code auth
225
+ // Use 'unset CLAUDECODE' to avoid nested session error
226
+ execSync(`tmux send-keys -t "${testSession}" "unset CLAUDECODE && claude -p 'test' 2>&1 | head -1" Enter`, { stdio: 'pipe' });
227
+ // Wait for response (Claude Code startup + auth check)
228
+ await new Promise(resolve => setTimeout(resolve, 3000));
229
+ // Capture output
230
+ const output = execSync(`tmux capture-pane -t "${testSession}" -p`, {
231
+ encoding: 'utf-8',
232
+ stdio: 'pipe'
233
+ });
234
+ // Clean up test session
235
+ execSync(`tmux kill-session -t "${testSession}"`, { stdio: 'pipe' });
236
+ // Check if auth failed
237
+ if (output.includes('Not logged in') || output.includes('Please run /login')) {
238
+ // Keychain access is broken - restart tmux server
239
+ // This happens silently - the next tmux session will have keychain access
240
+ execSync('tmux kill-server', { stdio: 'pipe' });
241
+ // Brief delay to ensure server fully stops
242
+ await new Promise(resolve => setTimeout(resolve, 500));
243
+ }
244
+ }
245
+ catch (_error) {
246
+ // Test session failed - clean up if it exists
247
+ try {
248
+ execSync(`tmux kill-session -t "${testSession}"`, { stdio: 'pipe' });
249
+ }
250
+ catch {
251
+ // Ignore cleanup errors
252
+ }
253
+ // Continue - worst case, spawn will fail with clear error message
254
+ }
255
+ }
155
256
  // =============================================================================
156
257
  // Executor Commands
157
258
  // =============================================================================
@@ -438,7 +539,8 @@ ${setTitleCmds}
438
539
  echo "🚀 Starting: ${sessionName}"
439
540
  echo ""
440
541
  cd "${context.worktreePath}"
441
- ${executorInvocation}
542
+ # Run executor in subshell with CLAUDECODE unset (prevents nested session error)
543
+ (unset CLAUDECODE CLAUDE_CODE_ENTRYPOINT; ${executorInvocation})
442
544
 
443
545
  # Clean up script and prompt files
444
546
  rm -f "$SCRIPT_PATH" "$PROMPT_PATH"
@@ -1693,11 +1795,13 @@ async function runDevcontainerInTmux(context, devcontainerCmd, config, displayMo
1693
1795
  // Create a script inside the container that runs claude and keeps shell open
1694
1796
  // TERM must be set for Claude's TUI to render properly
1695
1797
  // Unset CI to prevent Claude from detecting CI environment which suppresses TUI output
1798
+ // Unset CLAUDECODE to allow Claude Code to run (prevents nested session error)
1696
1799
  // Note: We keep DEVCONTAINER set so prlt workspace detection works correctly
1697
1800
  const tmuxScript = `#!/bin/bash
1698
1801
  export TERM=xterm-256color
1699
1802
  export COLORTERM=truecolor
1700
1803
  unset CI
1804
+ unset CLAUDECODE
1701
1805
  echo "🚀 Starting: ${sessionName}"
1702
1806
  echo ""
1703
1807
  ${claudeCmd}
@@ -2123,6 +2227,11 @@ export async function runVm(context, executor, config, host) {
2123
2227
  // Runner Dispatcher
2124
2228
  // =============================================================================
2125
2229
  export async function runExecution(environment, context, executor, config = DEFAULT_EXECUTION_CONFIG, options) {
2230
+ // Ensure tmux server has keychain access for OAuth (host only)
2231
+ // Docker uses claude-credentials volume, devcontainer runs inside container
2232
+ if (environment === 'host') {
2233
+ await ensureTmuxServerHasKeychainAccess();
2234
+ }
2126
2235
  switch (environment) {
2127
2236
  case 'devcontainer':
2128
2237
  return runDevcontainer(context, executor, config, options?.displayMode, options?.sessionManager);
@@ -16,6 +16,7 @@ import { hasDevcontainerConfig } from './devcontainer.js';
16
16
  import { loadExecutionConfig, getOrPromptCoderName } from './config.js';
17
17
  import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled, runExecutorPreflight, getAgentContainerName, isContainerRunning, getContainerId, buildSessionName } from './runners.js';
18
18
  import { detectRepoWorktrees, resolveWorktreePath } from './context.js';
19
+ import { ExternalExecutionMappingStore } from '../external-issues/mapping-store.js';
19
20
  import { generateBranchName, DEFAULT_EXECUTION_CONFIG, } from './types.js';
20
21
  // =============================================================================
21
22
  // Git Utilities
@@ -103,6 +104,12 @@ function findBaseBranchInContainer(containerId, containerRepoPath, candidates =
103
104
  * contention when all agents mount the same shared volumes concurrently.
104
105
  */
105
106
  const SPAWN_STAGGER_DELAY_MS = 2000;
107
+ const EXTERNAL_MAPPING_PROVIDERS = new Set(['linear', 'jira', 'asana', 'monday', 'pmo']);
108
+ function getExternalProvider(value) {
109
+ if (!value)
110
+ return null;
111
+ return EXTERNAL_MAPPING_PROVIDERS.has(value) ? value : null;
112
+ }
106
113
  // =============================================================================
107
114
  // Agent Selection
108
115
  // =============================================================================
@@ -501,6 +508,25 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
501
508
  await storage.moveTicket(ticket.projectId, ticket.id, inProgressColumn);
502
509
  }
503
510
  await autoExportToBoard(pmoPath, storage, log);
511
+ const externalProvider = getExternalProvider(ticket.metadata?.external_source);
512
+ const externalId = ticket.metadata?.external_id;
513
+ if (externalProvider && externalId) {
514
+ const mappingStore = new ExternalExecutionMappingStore(db);
515
+ mappingStore.upsertMapping({
516
+ provider: externalProvider,
517
+ externalId,
518
+ externalKey: ticket.metadata?.external_key ?? null,
519
+ canonicalUrl: ticket.metadata?.external_url ?? null,
520
+ latestStateSnapshot: {
521
+ ticketId: ticket.id,
522
+ ticketStatus: ticket.statusName ?? null,
523
+ ticketCategory: ticket.statusCategory ?? null,
524
+ teamKey: ticket.metadata?.external_project ?? null,
525
+ },
526
+ executionId: execution.id,
527
+ lastSpawnedAt: new Date(),
528
+ });
529
+ }
504
530
  return {
505
531
  success: true,
506
532
  executionId: execution.id,
@@ -25,6 +25,10 @@ export declare class ExecutionStorage {
25
25
  sessionId?: string;
26
26
  host?: string;
27
27
  logPath?: string;
28
+ externalSource?: string;
29
+ externalKey?: string;
30
+ externalId?: string;
31
+ externalUrl?: string;
28
32
  }): AgentWork;
29
33
  /**
30
34
  * Get execution by ID
@@ -26,6 +26,10 @@ function rowToAgentWork(row) {
26
26
  sessionId: row.session_id || undefined,
27
27
  host: row.host || undefined,
28
28
  logPath: row.log_path || undefined,
29
+ externalSource: row.external_source || undefined,
30
+ externalKey: row.external_key || undefined,
31
+ externalId: row.external_id || undefined,
32
+ externalUrl: row.external_url || undefined,
29
33
  startedAt: new Date(row.started_at),
30
34
  completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
31
35
  exitCode: row.exit_code ?? undefined,
@@ -52,9 +56,10 @@ export class ExecutionStorage {
52
56
  this.db.prepare(`
53
57
  INSERT INTO ${T.agent_work} (
54
58
  id, ticket_id, agent_name, executor, environment, display_mode, permission_mode,
55
- status, branch, pid, container_id, session_id, host, log_path, started_at
56
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 'starting', ?, ?, ?, ?, ?, ?, ?)
57
- `).run(id, params.ticketId, params.agentName, params.executor, params.environment, params.displayMode, params.permissionMode, params.branch || null, params.pid || null, params.containerId || null, params.sessionId || null, params.host || null, params.logPath || null, now);
59
+ status, branch, pid, container_id, session_id, host, log_path,
60
+ external_source, external_key, external_id, external_url, started_at
61
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'starting', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
62
+ `).run(id, params.ticketId, params.agentName, params.executor, params.environment, params.displayMode, params.permissionMode, params.branch || null, params.pid || null, params.containerId || null, params.sessionId || null, params.host || null, params.logPath || null, params.externalSource || null, params.externalKey || null, params.externalId || null, params.externalUrl || null, now);
58
63
  return this.getExecution(id);
59
64
  }
60
65
  /**
@@ -54,6 +54,10 @@ export interface AgentWork {
54
54
  sessionId?: string;
55
55
  host?: string;
56
56
  logPath?: string;
57
+ externalSource?: string;
58
+ externalKey?: string;
59
+ externalId?: string;
60
+ externalUrl?: string;
57
61
  startedAt: Date;
58
62
  completedAt?: Date;
59
63
  exitCode?: number;
@@ -1,4 +1,5 @@
1
- import { type ExternalIssueAdapter, type IssueEnvelope } from './types.js';
1
+ import { ExternalExecutionMappingStore } from './mapping-store.js';
2
+ import { type ExternalIssueAdapter, type ExternalExecutionMapping, type IssueEnvelope } from './types.js';
2
3
  type FetchIssueByKey = (key: string) => Promise<unknown>;
3
4
  type FetchIssuesByQuery = (query: Record<string, unknown>) => Promise<unknown[]>;
4
5
  interface AdapterFetchers {
@@ -13,6 +14,14 @@ export declare class LinearIssueAdapter implements ExternalIssueAdapter {
13
14
  normalize(raw: unknown): IssueEnvelope;
14
15
  fetchByKey(key: string): Promise<IssueEnvelope>;
15
16
  fetchByQuery(query: Record<string, unknown>): Promise<IssueEnvelope[]>;
17
+ persistMapping(store: ExternalExecutionMappingStore, envelope: IssueEnvelope, params?: {
18
+ executionId?: string;
19
+ prUrl?: string;
20
+ lastSyncedAt?: Date;
21
+ lastSpawnedAt?: Date;
22
+ }): ExternalExecutionMapping;
23
+ readMappingByExternalId(store: ExternalExecutionMappingStore, externalId: string): ExternalExecutionMapping | null;
24
+ readMappingsByExecutionId(store: ExternalExecutionMappingStore, executionId: string): ExternalExecutionMapping[];
16
25
  }
17
26
  export declare class JiraIssueAdapter implements ExternalIssueAdapter {
18
27
  readonly source: "jira";
@@ -22,5 +31,13 @@ export declare class JiraIssueAdapter implements ExternalIssueAdapter {
22
31
  normalize(raw: unknown): IssueEnvelope;
23
32
  fetchByKey(key: string): Promise<IssueEnvelope>;
24
33
  fetchByQuery(query: Record<string, unknown>): Promise<IssueEnvelope[]>;
34
+ persistMapping(store: ExternalExecutionMappingStore, envelope: IssueEnvelope, params?: {
35
+ executionId?: string;
36
+ prUrl?: string;
37
+ lastSyncedAt?: Date;
38
+ lastSpawnedAt?: Date;
39
+ }): ExternalExecutionMapping;
40
+ readMappingByExternalId(store: ExternalExecutionMappingStore, externalId: string): ExternalExecutionMapping | null;
41
+ readMappingsByExecutionId(store: ExternalExecutionMappingStore, executionId: string): ExternalExecutionMapping[];
25
42
  }
26
43
  export {};
@@ -1,5 +1,5 @@
1
1
  import { validateOrThrow } from './validation.js';
2
- import { ExternalIssueError } from './types.js';
2
+ import { ExternalIssueError, } from './types.js';
3
3
  function asRecord(value) {
4
4
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
5
5
  return {};
@@ -206,6 +206,30 @@ export class LinearIssueAdapter {
206
206
  const rawIssues = await fetchIssuesByQuery(query);
207
207
  return rawIssues.map((raw) => this.normalize(raw));
208
208
  }
209
+ persistMapping(store, envelope, params) {
210
+ return store.upsertMapping({
211
+ provider: this.source,
212
+ externalId: envelope.external_id,
213
+ externalKey: envelope.external_key,
214
+ canonicalUrl: envelope.url,
215
+ latestStateSnapshot: {
216
+ status: envelope.status,
217
+ priority: envelope.priority,
218
+ assignee: envelope.assignee,
219
+ projectKey: envelope.project_key,
220
+ },
221
+ executionId: params?.executionId,
222
+ prUrl: params?.prUrl,
223
+ lastSyncedAt: params?.lastSyncedAt,
224
+ lastSpawnedAt: params?.lastSpawnedAt,
225
+ });
226
+ }
227
+ readMappingByExternalId(store, externalId) {
228
+ return store.getByExternalId(this.source, externalId);
229
+ }
230
+ readMappingsByExecutionId(store, executionId) {
231
+ return store.findByExecutionId(executionId).filter((mapping) => mapping.provider === this.source);
232
+ }
209
233
  }
210
234
  export class JiraIssueAdapter {
211
235
  source = 'jira';
@@ -248,4 +272,28 @@ export class JiraIssueAdapter {
248
272
  const rawIssues = await fetchIssuesByQuery(query);
249
273
  return rawIssues.map((raw) => this.normalize(raw));
250
274
  }
275
+ persistMapping(store, envelope, params) {
276
+ return store.upsertMapping({
277
+ provider: this.source,
278
+ externalId: envelope.external_id,
279
+ externalKey: envelope.external_key,
280
+ canonicalUrl: envelope.url,
281
+ latestStateSnapshot: {
282
+ status: envelope.status,
283
+ priority: envelope.priority,
284
+ assignee: envelope.assignee,
285
+ projectKey: envelope.project_key,
286
+ },
287
+ executionId: params?.executionId,
288
+ prUrl: params?.prUrl,
289
+ lastSyncedAt: params?.lastSyncedAt,
290
+ lastSpawnedAt: params?.lastSpawnedAt,
291
+ });
292
+ }
293
+ readMappingByExternalId(store, externalId) {
294
+ return store.getByExternalId(this.source, externalId);
295
+ }
296
+ readMappingsByExecutionId(store, executionId) {
297
+ return store.findByExecutionId(executionId).filter((mapping) => mapping.provider === this.source);
298
+ }
251
299
  }
@@ -4,7 +4,10 @@
4
4
  * Shared contract for normalizing external issues (Linear, Jira) into
5
5
  * a canonical IssueEnvelope format with deterministic spawn context mapping.
6
6
  */
7
- export { type IssueSource, type IssueEnvelope, type IssueSpawnContext, type IssueValidationError, type IssueValidationErrorCode, type IssueValidationResult, type ExternalIssueAdapter, type ExternalIssueErrorCode, type ExternalIssueAdapterErrorCode, type NormalizedIssueEnvelope, type IssueSourceMetadata, ISSUE_SOURCES, ExternalIssueError, ExternalIssueAdapterError, toNormalizedEnvelope, } from './types.js';
7
+ export { type IssueSource, type IssueEnvelope, type IssueSpawnContext, type IssueValidationError, type IssueValidationErrorCode, type IssueValidationResult, type ExternalIssueAdapter, type ExternalIssueErrorCode, type ExternalIssueAdapterErrorCode, type NormalizedIssueEnvelope, type IssueSourceMetadata, type ExternalMappingProvider, type ExternalExecutionMapping, type UpsertExternalExecutionMappingInput, ISSUE_SOURCES, ExternalIssueError, ExternalIssueAdapterError, toNormalizedEnvelope, } from './types.js';
8
8
  export { validateIssueEnvelope, validateOrThrow, } from './validation.js';
9
9
  export { mapToSpawnContext, } from './mapper.js';
10
10
  export { LinearIssueAdapter, JiraIssueAdapter, } from './adapters.js';
11
+ export { ExternalExecutionMappingStore } from './mapping-store.js';
12
+ export { normalizeJiraIssue, normalizeJiraIssueToEnvelope, buildJiraTicketDescription, buildJiraMetadata, buildJiraSpawnContextMessage, getJiraIssueByKey, } from './jira.js';
13
+ export { resolveMirrorToPmo, type MirrorResolution, type MirrorResolutionInput, } from './work-start.js';
@@ -12,3 +12,8 @@ export { validateIssueEnvelope, validateOrThrow, } from './validation.js';
12
12
  export { mapToSpawnContext, } from './mapper.js';
13
13
  // Adapters
14
14
  export { LinearIssueAdapter, JiraIssueAdapter, } from './adapters.js';
15
+ // Mapping store
16
+ export { ExternalExecutionMappingStore } from './mapping-store.js';
17
+ // Source helpers
18
+ export { normalizeJiraIssue, normalizeJiraIssueToEnvelope, buildJiraTicketDescription, buildJiraMetadata, buildJiraSpawnContextMessage, getJiraIssueByKey, } from './jira.js';
19
+ export { resolveMirrorToPmo, } from './work-start.js';
@@ -0,0 +1,23 @@
1
+ import { type IssueEnvelope, type NormalizedIssueEnvelope } from './types.js';
2
+ export interface JiraAdapterConfig {
3
+ baseUrl?: string;
4
+ host?: string;
5
+ email?: string;
6
+ apiToken?: string;
7
+ projectKey?: string;
8
+ jql?: string;
9
+ }
10
+ export declare function normalizeJiraIssue(rawIssue: unknown): IssueEnvelope;
11
+ export declare function normalizeJiraIssueToEnvelope(rawIssue: unknown): NormalizedIssueEnvelope;
12
+ export declare function buildJiraTicketDescription(envelope: NormalizedIssueEnvelope): string;
13
+ export declare function buildJiraMetadata(envelope: NormalizedIssueEnvelope): Record<string, string>;
14
+ export declare function buildJiraSpawnContextMessage(envelope: NormalizedIssueEnvelope, additionalMessage?: string): string;
15
+ export declare function buildJiraIssueChoiceCommand(issueKey: string, projectId?: string): string;
16
+ export declare function getJiraIssueByKey(configInput: JiraAdapterConfig, issueKey: string, options?: {
17
+ fetchImpl?: typeof fetch;
18
+ }): Promise<NormalizedIssueEnvelope | null>;
19
+ export declare function listJiraIssues(configInput: JiraAdapterConfig, options?: {
20
+ limit?: number;
21
+ jql?: string;
22
+ fetchImpl?: typeof fetch;
23
+ }): Promise<NormalizedIssueEnvelope[]>;
@@ -0,0 +1,223 @@
1
+ import { ExternalIssueAdapterError, toNormalizedEnvelope, } from './types.js';
2
+ import { JiraIssueAdapter } from './adapters.js';
3
+ const DEFAULT_JIRA_API_PATH = '/rest/api/3';
4
+ const JIRA_ISSUE_FIELDS = [
5
+ 'summary',
6
+ 'description',
7
+ 'labels',
8
+ 'priority',
9
+ 'status',
10
+ 'project',
11
+ 'issuetype',
12
+ 'assignee',
13
+ ];
14
+ function normalizeBaseUrl(value) {
15
+ const trimmed = value.trim().replace(/\/+$/, '');
16
+ if (/^https?:\/\//i.test(trimmed)) {
17
+ return trimmed;
18
+ }
19
+ return `https://${trimmed}`;
20
+ }
21
+ function ensureJiraConfig(config) {
22
+ const baseUrl = config.baseUrl
23
+ || config.host
24
+ || process.env.PRLT_JIRA_BASE_URL
25
+ || process.env.JIRA_BASE_URL
26
+ || process.env.PRLT_JIRA_HOST
27
+ || process.env.JIRA_HOST;
28
+ const email = config.email || process.env.PRLT_JIRA_EMAIL || process.env.JIRA_EMAIL || '';
29
+ const apiToken = config.apiToken || process.env.PRLT_JIRA_API_TOKEN || process.env.JIRA_API_TOKEN;
30
+ const projectKey = config.projectKey || process.env.PRLT_JIRA_PROJECT || process.env.JIRA_PROJECT_KEY || '';
31
+ const jql = config.jql || process.env.PRLT_JIRA_JQL || '';
32
+ if (!baseUrl) {
33
+ throw new ExternalIssueAdapterError('MISSING_CONFIG', 'Missing Jira base URL. Set PRLT_JIRA_BASE_URL/JIRA_BASE_URL or PRLT_JIRA_HOST/JIRA_HOST.');
34
+ }
35
+ if (!apiToken) {
36
+ throw new ExternalIssueAdapterError('MISSING_CONFIG', 'Missing Jira API token. Set PRLT_JIRA_API_TOKEN or JIRA_API_TOKEN.');
37
+ }
38
+ return {
39
+ baseUrl: normalizeBaseUrl(baseUrl),
40
+ host: normalizeBaseUrl(baseUrl),
41
+ email,
42
+ apiToken,
43
+ projectKey,
44
+ jql,
45
+ };
46
+ }
47
+ function ensureIssueKey(key) {
48
+ const trimmed = key.trim().toUpperCase();
49
+ if (!/^[A-Z][A-Z0-9_]*-\d+$/.test(trimmed)) {
50
+ throw new ExternalIssueAdapterError('BAD_PAYLOAD', `Invalid Jira issue key: "${key}".`);
51
+ }
52
+ return trimmed;
53
+ }
54
+ function buildAuthHeader(config) {
55
+ if (config.email) {
56
+ const token = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
57
+ return `Basic ${token}`;
58
+ }
59
+ return `Bearer ${config.apiToken}`;
60
+ }
61
+ function buildListJql(config, explicitJql) {
62
+ const jql = explicitJql?.trim() || config.jql.trim();
63
+ if (jql.length > 0) {
64
+ return jql;
65
+ }
66
+ const projectKey = config.projectKey.trim();
67
+ if (!projectKey) {
68
+ throw new ExternalIssueAdapterError('MISSING_CONFIG', 'Missing Jira project key/JQL. Pass --project-key or --jql, or set PRLT_JIRA_PROJECT/PRLT_JIRA_JQL.');
69
+ }
70
+ return `project = "${projectKey}" AND statusCategory != Done ORDER BY updated DESC`;
71
+ }
72
+ function getErrorMessage(payload, fallback) {
73
+ if (!payload || typeof payload !== 'object') {
74
+ return fallback;
75
+ }
76
+ const errorMessages = payload.errorMessages;
77
+ if (Array.isArray(errorMessages) && typeof errorMessages[0] === 'string' && errorMessages[0].trim()) {
78
+ return errorMessages[0].trim();
79
+ }
80
+ return fallback;
81
+ }
82
+ async function parseJsonOrThrow(response) {
83
+ try {
84
+ return await response.json();
85
+ }
86
+ catch {
87
+ throw new ExternalIssueAdapterError('BAD_PAYLOAD', 'Jira response was not valid JSON.');
88
+ }
89
+ }
90
+ function deriveJiraCategory(envelope) {
91
+ const typeValue = envelope.item_type?.trim().toLowerCase() || '';
92
+ if (typeValue === 'bug' || typeValue === 'incident') {
93
+ return 'bug';
94
+ }
95
+ if (typeValue === 'task' || typeValue === 'story' || typeValue === 'issue') {
96
+ return 'feature';
97
+ }
98
+ return 'feature';
99
+ }
100
+ function ensureIssueShape(issue) {
101
+ if (!issue.id || !issue.key || !issue.fields) {
102
+ throw new ExternalIssueAdapterError('BAD_PAYLOAD', 'Jira issue payload is missing required fields (id, key, fields).', issue);
103
+ }
104
+ }
105
+ export function normalizeJiraIssue(rawIssue) {
106
+ if (!rawIssue || typeof rawIssue !== 'object') {
107
+ throw new ExternalIssueAdapterError('BAD_PAYLOAD', 'Jira issue payload is invalid.', rawIssue);
108
+ }
109
+ const issue = rawIssue;
110
+ ensureIssueShape(issue);
111
+ const adapter = new JiraIssueAdapter();
112
+ return adapter.normalize(issue);
113
+ }
114
+ export function normalizeJiraIssueToEnvelope(rawIssue) {
115
+ const envelope = normalizeJiraIssue(rawIssue);
116
+ return toNormalizedEnvelope(envelope, deriveJiraCategory(envelope));
117
+ }
118
+ export function buildJiraTicketDescription(envelope) {
119
+ const body = envelope.description.trim();
120
+ const metadataLines = [
121
+ `- Source: ${envelope.source.name}`,
122
+ `- External key: ${envelope.source.externalKey}`,
123
+ `- External id: ${envelope.source.externalId}`,
124
+ `- URL: ${envelope.source.url}`,
125
+ `- Status: ${envelope.status}`,
126
+ `- Priority: ${envelope.priority || 'Unset'}`,
127
+ `- Labels: ${envelope.labels.length > 0 ? envelope.labels.join(', ') : 'None'}`,
128
+ ];
129
+ const parts = [
130
+ body,
131
+ '## External Issue Context',
132
+ metadataLines.join('\n'),
133
+ ].filter(Boolean);
134
+ return parts.join('\n\n');
135
+ }
136
+ export function buildJiraMetadata(envelope) {
137
+ return {
138
+ external_source: envelope.source.name,
139
+ external_key: envelope.source.externalKey,
140
+ external_id: envelope.source.externalId,
141
+ external_url: envelope.source.url,
142
+ external_raw: JSON.stringify(envelope.source.raw),
143
+ };
144
+ }
145
+ export function buildJiraSpawnContextMessage(envelope, additionalMessage) {
146
+ const lines = [
147
+ `External issue source: ${envelope.source.name}`,
148
+ `External issue key: ${envelope.source.externalKey}`,
149
+ `External issue id: ${envelope.source.externalId}`,
150
+ `External issue URL: ${envelope.source.url}`,
151
+ ];
152
+ if (additionalMessage?.trim()) {
153
+ lines.push('', additionalMessage.trim());
154
+ }
155
+ return lines.join('\n');
156
+ }
157
+ export function buildJiraIssueChoiceCommand(issueKey, projectId) {
158
+ let command = `prlt work jira --issue ${issueKey} --json`;
159
+ if (projectId) {
160
+ command += ` -P ${projectId}`;
161
+ }
162
+ return command;
163
+ }
164
+ export async function getJiraIssueByKey(configInput, issueKey, options) {
165
+ const config = ensureJiraConfig(configInput);
166
+ const fetchImpl = options?.fetchImpl || fetch;
167
+ const key = ensureIssueKey(issueKey);
168
+ const baseApiUrl = `${config.baseUrl}${DEFAULT_JIRA_API_PATH}`;
169
+ const fields = encodeURIComponent(JIRA_ISSUE_FIELDS.join(','));
170
+ const issueUrl = `${baseApiUrl}/issue/${encodeURIComponent(key)}?fields=${fields}`;
171
+ const response = await fetchImpl(issueUrl, {
172
+ method: 'GET',
173
+ headers: {
174
+ Accept: 'application/json',
175
+ Authorization: buildAuthHeader(config),
176
+ },
177
+ });
178
+ if (response.status === 401 || response.status === 403) {
179
+ throw new ExternalIssueAdapterError('AUTH_FAILED', 'Jira authentication failed. Verify your Jira credentials.');
180
+ }
181
+ if (response.status === 404) {
182
+ return null;
183
+ }
184
+ const payload = await parseJsonOrThrow(response);
185
+ if (!response.ok) {
186
+ const msg = getErrorMessage(payload, `Jira request failed with status ${response.status}.`);
187
+ throw new ExternalIssueAdapterError('REQUEST_FAILED', msg, payload);
188
+ }
189
+ return normalizeJiraIssueToEnvelope(payload);
190
+ }
191
+ export async function listJiraIssues(configInput, options) {
192
+ const config = ensureJiraConfig(configInput);
193
+ const fetchImpl = options?.fetchImpl || fetch;
194
+ const limit = Math.max(1, Math.min(options?.limit ?? 20, 100));
195
+ const jql = buildListJql(config, options?.jql);
196
+ const baseApiUrl = `${config.baseUrl}${DEFAULT_JIRA_API_PATH}`;
197
+ const response = await fetchImpl(`${baseApiUrl}/search`, {
198
+ method: 'POST',
199
+ headers: {
200
+ 'Content-Type': 'application/json',
201
+ Accept: 'application/json',
202
+ Authorization: buildAuthHeader(config),
203
+ },
204
+ body: JSON.stringify({
205
+ jql,
206
+ maxResults: limit,
207
+ fields: JIRA_ISSUE_FIELDS,
208
+ }),
209
+ });
210
+ if (response.status === 401 || response.status === 403) {
211
+ throw new ExternalIssueAdapterError('AUTH_FAILED', 'Jira authentication failed. Verify your Jira credentials.');
212
+ }
213
+ const payload = await parseJsonOrThrow(response);
214
+ if (!response.ok) {
215
+ const msg = getErrorMessage(payload, `Jira request failed with status ${response.status}.`);
216
+ throw new ExternalIssueAdapterError('REQUEST_FAILED', msg, payload);
217
+ }
218
+ const data = payload;
219
+ if (!Array.isArray(data.issues)) {
220
+ throw new ExternalIssueAdapterError('BAD_PAYLOAD', 'Jira response payload was missing issues array.', payload);
221
+ }
222
+ return data.issues.map(normalizeJiraIssueToEnvelope);
223
+ }
@@ -65,14 +65,15 @@ function priorityFromLinear(value) {
65
65
  return null;
66
66
  }
67
67
  }
68
- function ensureLinearConfig(config) {
68
+ function ensureLinearConfig(config, options) {
69
69
  const apiKey = config.apiKey || process.env.LINEAR_API_KEY || process.env.PRLT_LINEAR_API_KEY;
70
70
  const team = config.team || process.env.PRLT_LINEAR_TEAM || process.env.LINEAR_TEAM_KEY;
71
71
  const apiUrl = config.apiUrl || process.env.PRLT_LINEAR_API_URL || DEFAULT_LINEAR_API_URL;
72
72
  if (!apiKey) {
73
73
  throw new ExternalIssueAdapterError('MISSING_CONFIG', 'Missing Linear API key. Set LINEAR_API_KEY or PRLT_LINEAR_API_KEY.');
74
74
  }
75
- if (!team) {
75
+ const requireTeam = options?.requireTeam ?? true;
76
+ if (requireTeam && !team) {
76
77
  throw new ExternalIssueAdapterError('MISSING_CONFIG', 'Missing Linear team key. Pass --team or set PRLT_LINEAR_TEAM.');
77
78
  }
78
79
  return { apiKey, team, apiUrl };
@@ -180,7 +181,7 @@ export function buildLinearIssueChoiceCommand(issueIdentifier, projectId) {
180
181
  * Fetch a single Linear issue by identifier (for example, ENG-123) and normalize it.
181
182
  */
182
183
  export async function getLinearIssueByIdentifier(configInput, identifier, options) {
183
- const config = ensureLinearConfig(configInput);
184
+ const config = ensureLinearConfig(configInput, { requireTeam: false });
184
185
  const fetchImpl = options?.fetchImpl || fetch;
185
186
  const response = await fetchImpl(config.apiUrl, {
186
187
  method: 'POST',
@@ -15,7 +15,8 @@ import type { IssueEnvelope, IssueSpawnContext } from './types.js';
15
15
  * The mapping is deterministic: the same IssueEnvelope always produces
16
16
  * the same IssueSpawnContext.
17
17
  *
18
- * @param envelope - Validated IssueEnvelope
18
+ * @param input - IssueEnvelope-like input
19
19
  * @returns Spawn context with prompt and metadata
20
+ * @throws ExternalIssueError when input fails IssueEnvelope validation
20
21
  */
21
- export declare function mapToSpawnContext(envelope: IssueEnvelope): IssueSpawnContext;
22
+ export declare function mapToSpawnContext(input: IssueEnvelope | unknown): IssueSpawnContext;