@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
@@ -13,10 +13,14 @@ import { getWorkspaceInfo, createEphemeralAgent, getTicketTmuxSession, killTmuxS
13
13
  import { generateBranchName, DEFAULT_EXECUTION_CONFIG, } from '../../lib/execution/types.js';
14
14
  import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled, dockerCredentialsExist, getDockerCredentialInfo, isClaudeExecutor, getExecutorDisplayName } from '../../lib/execution/runners.js';
15
15
  import { ExecutionStorage, ContainerStorage } from '../../lib/execution/storage.js';
16
- import { loadExecutionConfig, getTerminalApp, promptTerminalPreference, getShell, promptShellPreference, hasTerminalPreference, hasShellPreference, getOrPromptCoderName, getAuthMethod, saveAuthMethod, getCreatePrDefault } from '../../lib/execution/config.js';
16
+ import { loadExecutionConfig, getTerminalApp, promptTerminalPreference, getShell, promptShellPreference, hasTerminalPreference, hasShellPreference, getOrPromptCoderName, getAuthMethod, saveAuthMethod, getCreatePrDefault, getMirrorToPmoDefault } from '../../lib/execution/config.js';
17
17
  import { hasDevcontainerConfig } from '../../lib/execution/devcontainer.js';
18
18
  import { detectRepoWorktrees, resolveWorktreePath } from '../../lib/execution/context.js';
19
19
  import { isGHInstalled, isGHAuthenticated } from '../../lib/pr/index.js';
20
+ import { buildLinearMetadata, buildLinearSpawnContextMessage, buildLinearTicketDescription, getLinearIssueByIdentifier, } from '../../lib/external-issues/linear.js';
21
+ import { buildJiraMetadata, buildJiraSpawnContextMessage, buildJiraTicketDescription, getJiraIssueByKey, } from '../../lib/external-issues/jira.js';
22
+ import { resolveMirrorToPmo } from '../../lib/external-issues/work-start.js';
23
+ import { ExternalIssueAdapterError } from '../../lib/external-issues/types.js';
20
24
  /**
21
25
  * Try to execute a git command, return true if successful
22
26
  */
@@ -67,6 +71,49 @@ function getActiveStaffAgents(workspaceInfo, log) {
67
71
  }
68
72
  return result;
69
73
  }
74
+ function parseBooleanSetting(value) {
75
+ if (!value)
76
+ return null;
77
+ const normalized = value.trim().toLowerCase();
78
+ if (normalized === 'true')
79
+ return true;
80
+ if (normalized === 'false')
81
+ return false;
82
+ return null;
83
+ }
84
+ function isIssueSource(value) {
85
+ return value === 'linear' || value === 'jira';
86
+ }
87
+ function buildExternalMetadata(envelope) {
88
+ return envelope.source.name === 'jira'
89
+ ? buildJiraMetadata(envelope)
90
+ : buildLinearMetadata(envelope);
91
+ }
92
+ function buildExternalTicketDescription(envelope) {
93
+ return envelope.source.name === 'jira'
94
+ ? buildJiraTicketDescription(envelope)
95
+ : buildLinearTicketDescription(envelope);
96
+ }
97
+ function buildExternalSpawnContextMessage(envelope, additionalMessage) {
98
+ return envelope.source.name === 'jira'
99
+ ? buildJiraSpawnContextMessage(envelope, additionalMessage)
100
+ : buildLinearSpawnContextMessage(envelope, additionalMessage);
101
+ }
102
+ function getTicketExternalMetadata(ticket) {
103
+ const metadata = (typeof ticket === 'object'
104
+ && ticket !== null
105
+ && 'metadata' in ticket
106
+ && typeof ticket.metadata === 'object'
107
+ && ticket.metadata !== null
108
+ ? ticket.metadata
109
+ : {});
110
+ return {
111
+ source: typeof metadata.external_source === 'string' ? metadata.external_source : undefined,
112
+ key: typeof metadata.external_key === 'string' ? metadata.external_key : undefined,
113
+ id: typeof metadata.external_id === 'string' ? metadata.external_id : undefined,
114
+ url: typeof metadata.external_url === 'string' ? metadata.external_url : undefined,
115
+ };
116
+ }
70
117
  export default class WorkStart extends PMOCommand {
71
118
  static description = 'Start work on a ticket (launches an agent to implement it)';
72
119
  static examples = [
@@ -78,6 +125,8 @@ export default class WorkStart extends PMOCommand {
78
125
  '<%= config.bin %> <%= command.id %> # Interactive mode',
79
126
  '<%= config.bin %> <%= command.id %> --all # Spawn all backlog tickets',
80
127
  '<%= config.bin %> <%= command.id %> TKT-001 --prompt "Add unit tests for the API" # Custom prompt',
128
+ '<%= config.bin %> <%= command.id %> --from-issue --source linear --key ENG-123',
129
+ '<%= config.bin %> <%= command.id %> --from-issue --source jira --key PROJ-123 --mirror-to-pmo',
81
130
  ];
82
131
  static args = {
83
132
  ticketId: Args.string({
@@ -108,6 +157,21 @@ export default class WorkStart extends PMOCommand {
108
157
  message: Flags.string({
109
158
  description: 'Additional instructions appended to any action prompt',
110
159
  }),
160
+ 'from-issue': Flags.boolean({
161
+ description: 'Start from external issue source instead of internal ticket id',
162
+ default: false,
163
+ }),
164
+ source: Flags.string({
165
+ description: 'External issue source',
166
+ options: ['linear', 'jira'],
167
+ }),
168
+ key: Flags.string({
169
+ description: 'External issue key (for example: ENG-123, PROJ-456)',
170
+ }),
171
+ 'mirror-to-pmo': Flags.boolean({
172
+ description: 'Mirror external issue data into PMO ticket (default from execution.mirror_to_pmo_default or PRLT_MIRROR_TO_PMO_DEFAULT)',
173
+ allowNo: true,
174
+ }),
111
175
  watch: Flags.boolean({
112
176
  char: 'w',
113
177
  description: 'Stream output in real-time',
@@ -187,9 +251,95 @@ export default class WorkStart extends PMOCommand {
187
251
  hidden: true,
188
252
  }),
189
253
  };
254
+ async findLinkedTicketByEnvelope(projectId, envelope) {
255
+ const tickets = await this.storage.listTickets(projectId);
256
+ return tickets.find((ticket) => {
257
+ const external = getTicketExternalMetadata(ticket);
258
+ return external.source === envelope.source.name
259
+ && (external.key === envelope.source.externalKey || external.id === envelope.source.externalId);
260
+ });
261
+ }
262
+ async createOrUpdateLinkedTicket(projectId, envelope) {
263
+ const existing = await this.findLinkedTicketByEnvelope(projectId, envelope);
264
+ const description = buildExternalTicketDescription(envelope);
265
+ const metadata = buildExternalMetadata(envelope);
266
+ if (existing) {
267
+ return this.storage.updateTicket(existing.id, {
268
+ title: envelope.title,
269
+ description,
270
+ priority: envelope.priority ?? undefined,
271
+ category: envelope.category ?? undefined,
272
+ labels: envelope.labels,
273
+ metadata: {
274
+ ...existing.metadata,
275
+ ...metadata,
276
+ },
277
+ });
278
+ }
279
+ return this.storage.createTicket(projectId, {
280
+ title: envelope.title,
281
+ description,
282
+ priority: envelope.priority ?? undefined,
283
+ category: envelope.category ?? undefined,
284
+ labels: envelope.labels,
285
+ metadata,
286
+ });
287
+ }
288
+ async fetchExternalIssue(source, key) {
289
+ if (source === 'jira') {
290
+ return getJiraIssueByKey({}, key);
291
+ }
292
+ return getLinearIssueByIdentifier({}, key);
293
+ }
294
+ async resolveIssueSourceAndKey(input, jsonMode) {
295
+ let source = input.source;
296
+ let key = input.key;
297
+ const sourceResolver = new FlagResolver({
298
+ commandName: 'work start',
299
+ baseCommand: 'prlt work start --from-issue',
300
+ jsonMode,
301
+ flags: {},
302
+ });
303
+ sourceResolver.addPrompt({
304
+ flagName: 'source',
305
+ type: 'list',
306
+ message: 'Select external issue source:',
307
+ default: isIssueSource(source) ? source : undefined,
308
+ when: () => !isIssueSource(source),
309
+ choices: () => [
310
+ { name: 'Linear', value: 'linear', command: 'prlt work start --from-issue --source linear --json' },
311
+ { name: 'Jira', value: 'jira', command: 'prlt work start --from-issue --source jira --json' },
312
+ ],
313
+ });
314
+ const sourceResult = await sourceResolver.resolve();
315
+ source = source ?? sourceResult.source;
316
+ if (!isIssueSource(source)) {
317
+ throw new Error('Invalid source');
318
+ }
319
+ const keyResolver = new FlagResolver({
320
+ commandName: 'work start',
321
+ baseCommand: `prlt work start --from-issue --source ${source}`,
322
+ jsonMode,
323
+ flags: {},
324
+ });
325
+ keyResolver.addPrompt({
326
+ flagName: 'key',
327
+ type: 'input',
328
+ message: `Enter ${source === 'linear' ? 'Linear' : 'Jira'} issue key:`,
329
+ default: key,
330
+ when: () => !key?.trim(),
331
+ validate: (value) => value.trim().length > 0 ? true : 'Issue key is required',
332
+ });
333
+ const keyResult = await keyResolver.resolve();
334
+ const resolvedKey = (key ?? keyResult.key ?? '').trim();
335
+ if (!resolvedKey) {
336
+ throw new Error('Issue key is required');
337
+ }
338
+ return { source, key: resolvedKey };
339
+ }
190
340
  async execute() {
191
341
  const { args, flags } = await this.parse(WorkStart);
192
- const projectId = flags.project;
342
+ let projectId = flags.project;
193
343
  // Check for conflicting PR flags
194
344
  if (flags['create-pr'] && flags['no-pr']) {
195
345
  this.error('--create-pr and --no-pr are mutually exclusive');
@@ -239,6 +389,71 @@ export default class WorkStart extends PMOCommand {
239
389
  }
240
390
  // Get ticketId - prompt if not provided
241
391
  let ticketId = args.ticketId;
392
+ let externalIssueContextMessage;
393
+ let fromIssueMirror;
394
+ let fromIssueMirrorSource;
395
+ if (flags['from-issue']) {
396
+ if (ticketId) {
397
+ db.close();
398
+ return handleError('INVALID_FLAGS', 'Cannot provide a ticket ID positional argument when using --from-issue.');
399
+ }
400
+ projectId = projectId || await this.requireProject({
401
+ jsonMode: {
402
+ flags,
403
+ commandName: 'work start',
404
+ baseCommand: 'prlt work start --from-issue',
405
+ },
406
+ });
407
+ const sourceAndKey = await this.resolveIssueSourceAndKey({
408
+ source: flags.source,
409
+ key: flags.key,
410
+ }, jsonMode);
411
+ const envMirrorDefault = parseBooleanSetting(process.env.PRLT_MIRROR_TO_PMO_DEFAULT);
412
+ const configMirrorDefault = getMirrorToPmoDefault(db);
413
+ const mirrorResolution = resolveMirrorToPmo({
414
+ flagValue: flags['mirror-to-pmo'],
415
+ envValue: envMirrorDefault,
416
+ configValue: configMirrorDefault,
417
+ });
418
+ const mirrorToPmo = mirrorResolution.enabled;
419
+ fromIssueMirror = mirrorToPmo;
420
+ fromIssueMirrorSource = mirrorResolution.source;
421
+ if (!jsonMode) {
422
+ this.log(styles.muted(`External issue mirror: ${mirrorToPmo ? 'enabled' : 'disabled'} (${mirrorResolution.source})`));
423
+ }
424
+ let envelope = null;
425
+ try {
426
+ envelope = await this.fetchExternalIssue(sourceAndKey.source, sourceAndKey.key);
427
+ }
428
+ catch (error) {
429
+ if (error instanceof ExternalIssueAdapterError) {
430
+ db.close();
431
+ return handleError(`EXTERNAL_ISSUE_${error.code}`, `[${sourceAndKey.source}] ${error.message}`);
432
+ }
433
+ const message = error instanceof Error ? error.message : 'Failed to fetch external issue.';
434
+ db.close();
435
+ return handleError('EXTERNAL_ISSUE_REQUEST_FAILED', message);
436
+ }
437
+ if (!envelope) {
438
+ db.close();
439
+ return handleError('EXTERNAL_ISSUE_NOT_FOUND', `${sourceAndKey.source} issue "${sourceAndKey.key}" was not found.`);
440
+ }
441
+ const existingLinkedTicket = await this.findLinkedTicketByEnvelope(projectId, envelope);
442
+ let linkedTicket;
443
+ if (mirrorToPmo) {
444
+ linkedTicket = await this.createOrUpdateLinkedTicket(projectId, envelope);
445
+ await autoExportToBoard(this.pmoPath, this.storage);
446
+ }
447
+ else {
448
+ if (!existingLinkedTicket) {
449
+ db.close();
450
+ return handleError('EXTERNAL_ISSUE_NOT_MIRRORED', `No linked PMO ticket found for ${sourceAndKey.source} issue "${sourceAndKey.key}". Re-run with --mirror-to-pmo.`);
451
+ }
452
+ linkedTicket = existingLinkedTicket;
453
+ }
454
+ ticketId = linkedTicket.id;
455
+ externalIssueContextMessage = buildExternalSpawnContextMessage(envelope, flags.message);
456
+ }
242
457
  if (!ticketId) {
243
458
  // Get all tickets, optionally filtered by project if -P/--project flag is provided
244
459
  const allTickets = await this.storage.listTickets(projectId);
@@ -285,6 +500,19 @@ export default class WorkStart extends PMOCommand {
285
500
  : earlyConfigPrDefault === false ? 'no-pr'
286
501
  : 'no-pr';
287
502
  metadata.resolvedPRMode = earlyResolvedPr;
503
+ const externalMetadata = getTicketExternalMetadata(ticket);
504
+ if (externalMetadata.source || externalMetadata.key) {
505
+ metadata.externalIssue = {
506
+ source: externalMetadata.source ?? null,
507
+ key: externalMetadata.key ?? null,
508
+ id: externalMetadata.id ?? null,
509
+ url: externalMetadata.url ?? null,
510
+ };
511
+ }
512
+ if (flags['from-issue']) {
513
+ metadata.mirrorToPmo = fromIssueMirror ?? null;
514
+ metadata.mirrorToPmoSource = fromIssueMirrorSource ?? null;
515
+ }
288
516
  // Build the confirm command with --yes
289
517
  let confirmCmd = `prlt work start ${ticketId}`;
290
518
  if (flags.action)
@@ -796,7 +1024,7 @@ export default class WorkStart extends PMOCommand {
796
1024
  actionEndPrompt: customPrompt ? undefined : selectedAction?.endPrompt,
797
1025
  modifiesCode: customPrompt ? true : selectedAction?.modifiesCode ?? true,
798
1026
  // Additional instructions from --message flag
799
- customMessage: flags.message,
1027
+ customMessage: externalIssueContextMessage ?? flags.message,
800
1028
  };
801
1029
  // Check if agent has devcontainer config
802
1030
  const hasDevcontainer = hasDevcontainerConfig(agentDir);
@@ -1523,6 +1751,7 @@ export default class WorkStart extends PMOCommand {
1523
1751
  }
1524
1752
  }
1525
1753
  // Create execution record
1754
+ const ticketExternalMetadata = getTicketExternalMetadata(ticket);
1526
1755
  const execution = executionStorage.createExecution({
1527
1756
  ticketId: ticket.id,
1528
1757
  agentName: assignedAgent,
@@ -1531,6 +1760,10 @@ export default class WorkStart extends PMOCommand {
1531
1760
  displayMode,
1532
1761
  permissionMode,
1533
1762
  branch,
1763
+ externalSource: ticketExternalMetadata.source,
1764
+ externalKey: ticketExternalMetadata.key,
1765
+ externalId: ticketExternalMetadata.id,
1766
+ externalUrl: ticketExternalMetadata.url,
1534
1767
  });
1535
1768
  if (!jsonMode) {
1536
1769
  this.log(styles.muted(` Work ID: ${execution.id}`));
@@ -2027,6 +2260,7 @@ export default class WorkStart extends PMOCommand {
2027
2260
  }
2028
2261
  }
2029
2262
  // Create execution record
2263
+ const ticketExternalMetadata = getTicketExternalMetadata(ticket);
2030
2264
  const execution = executionStorage.createExecution({
2031
2265
  ticketId: ticket.id,
2032
2266
  agentName,
@@ -2035,6 +2269,10 @@ export default class WorkStart extends PMOCommand {
2035
2269
  displayMode,
2036
2270
  permissionMode,
2037
2271
  branch,
2272
+ externalSource: ticketExternalMetadata.source,
2273
+ externalKey: ticketExternalMetadata.key,
2274
+ externalId: ticketExternalMetadata.id,
2275
+ externalUrl: ticketExternalMetadata.url,
2038
2276
  });
2039
2277
  // Note: Ticket status update moved to after successful spawn
2040
2278
  // Load execution config
@@ -0,0 +1,15 @@
1
+ import type { AsanaProject, AsanaTask, AsanaTaskUpsertInput, AsanaUser, AsanaWorkspace } from './types.js';
2
+ export declare class AsanaClient {
3
+ private accessToken;
4
+ private readonly baseUrl;
5
+ constructor(accessToken: string);
6
+ private request;
7
+ verify(): Promise<AsanaUser>;
8
+ listWorkspaces(): Promise<AsanaWorkspace[]>;
9
+ getWorkspace(workspaceGid: string): Promise<AsanaWorkspace | null>;
10
+ listProjects(workspaceGid: string): Promise<AsanaProject[]>;
11
+ getProject(projectGid: string): Promise<AsanaProject | null>;
12
+ getTask(taskGid: string): Promise<AsanaTask | null>;
13
+ createTask(input: AsanaTaskUpsertInput): Promise<AsanaTask>;
14
+ updateTask(taskGid: string, input: AsanaTaskUpsertInput): Promise<AsanaTask>;
15
+ }
@@ -0,0 +1,120 @@
1
+ export class AsanaClient {
2
+ accessToken;
3
+ baseUrl = 'https://app.asana.com/api/1.0';
4
+ constructor(accessToken) {
5
+ this.accessToken = accessToken;
6
+ }
7
+ async request(path, init = {}) {
8
+ const url = new URL(`${this.baseUrl}${path}`);
9
+ if (init.query) {
10
+ for (const [key, value] of Object.entries(init.query)) {
11
+ if (value !== undefined) {
12
+ url.searchParams.set(key, String(value));
13
+ }
14
+ }
15
+ }
16
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins -- fetch is available in supported Node runtimes for this CLI
17
+ const response = await fetch(url, {
18
+ method: init.method ?? 'GET',
19
+ headers: {
20
+ Authorization: `Bearer ${this.accessToken}`,
21
+ 'Content-Type': 'application/json',
22
+ },
23
+ body: init.body ? JSON.stringify(init.body) : undefined,
24
+ });
25
+ const payload = await response.json();
26
+ if (!response.ok) {
27
+ const errorMessage = payload?.errors?.[0]?.message ?? `Asana API request failed (${response.status})`;
28
+ throw new Error(errorMessage);
29
+ }
30
+ return payload.data;
31
+ }
32
+ async verify() {
33
+ const user = await this.request('/users/me', {
34
+ query: { opt_fields: 'gid,name,email,workspaces.gid,workspaces.name' },
35
+ });
36
+ return {
37
+ gid: user.gid,
38
+ name: user.name,
39
+ email: user.email,
40
+ workspaces: (user.workspaces ?? []).map((workspace) => ({
41
+ gid: workspace.gid,
42
+ name: workspace.name,
43
+ })),
44
+ };
45
+ }
46
+ async listWorkspaces() {
47
+ const user = await this.verify();
48
+ return user.workspaces;
49
+ }
50
+ async getWorkspace(workspaceGid) {
51
+ try {
52
+ const workspace = await this.request(`/workspaces/${workspaceGid}`);
53
+ return { gid: workspace.gid, name: workspace.name };
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ async listProjects(workspaceGid) {
60
+ const projects = await this.request('/projects', {
61
+ query: {
62
+ workspace: workspaceGid,
63
+ archived: false,
64
+ opt_fields: 'gid,name',
65
+ },
66
+ });
67
+ return projects.map((project) => ({ gid: project.gid, name: project.name }));
68
+ }
69
+ async getProject(projectGid) {
70
+ try {
71
+ const project = await this.request(`/projects/${projectGid}`, {
72
+ query: { opt_fields: 'gid,name' },
73
+ });
74
+ return { gid: project.gid, name: project.name };
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ async getTask(taskGid) {
81
+ try {
82
+ const task = await this.request(`/tasks/${taskGid}`, {
83
+ query: { opt_fields: 'gid,name,completed,notes' },
84
+ });
85
+ return {
86
+ gid: task.gid,
87
+ name: task.name,
88
+ completed: task.completed,
89
+ notes: task.notes,
90
+ };
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ async createTask(input) {
97
+ const task = await this.request('/tasks', {
98
+ method: 'POST',
99
+ body: { data: input },
100
+ });
101
+ return {
102
+ gid: task.gid,
103
+ name: task.name,
104
+ completed: task.completed,
105
+ notes: task.notes,
106
+ };
107
+ }
108
+ async updateTask(taskGid, input) {
109
+ const task = await this.request(`/tasks/${taskGid}`, {
110
+ method: 'PUT',
111
+ body: { data: input },
112
+ });
113
+ return {
114
+ gid: task.gid,
115
+ name: task.name,
116
+ completed: task.completed,
117
+ notes: task.notes,
118
+ };
119
+ }
120
+ }
@@ -0,0 +1,9 @@
1
+ import Database from 'better-sqlite3';
2
+ import type { AsanaConfig } from './types.js';
3
+ export declare function isAsanaConfigured(db: Database.Database): boolean;
4
+ export declare function loadAsanaConfig(db: Database.Database): AsanaConfig | null;
5
+ export declare function saveAsanaAccessToken(db: Database.Database, accessToken: string): void;
6
+ export declare function saveAsanaWorkspace(db: Database.Database, workspaceGid: string, workspaceName: string): void;
7
+ export declare function saveAsanaProject(db: Database.Database, projectGid: string, projectName: string): void;
8
+ export declare function clearAsanaConfig(db: Database.Database): void;
9
+ export declare function getAsanaAccessToken(db: Database.Database): string | null;
@@ -0,0 +1,61 @@
1
+ const SETTINGS_TABLE = 'workspace_settings';
2
+ const ASANA_CONFIG_KEYS = {
3
+ accessToken: 'asana.access_token',
4
+ workspaceGid: 'asana.workspace_gid',
5
+ workspaceName: 'asana.workspace_name',
6
+ projectGid: 'asana.project_gid',
7
+ projectName: 'asana.project_name',
8
+ };
9
+ function getSetting(db, key) {
10
+ const row = db
11
+ .prepare(`SELECT value FROM ${SETTINGS_TABLE} WHERE key = ?`)
12
+ .get(key);
13
+ return row?.value ?? null;
14
+ }
15
+ function setSetting(db, key, value) {
16
+ db.prepare(`
17
+ INSERT INTO ${SETTINGS_TABLE} (key, value)
18
+ VALUES (?, ?)
19
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
20
+ `).run(key, value);
21
+ }
22
+ function deleteSetting(db, key) {
23
+ db.prepare(`DELETE FROM ${SETTINGS_TABLE} WHERE key = ?`).run(key);
24
+ }
25
+ export function isAsanaConfigured(db) {
26
+ return getSetting(db, ASANA_CONFIG_KEYS.accessToken) !== null;
27
+ }
28
+ export function loadAsanaConfig(db) {
29
+ const accessToken = getSetting(db, ASANA_CONFIG_KEYS.accessToken);
30
+ if (!accessToken)
31
+ return null;
32
+ return {
33
+ accessToken,
34
+ workspaceGid: getSetting(db, ASANA_CONFIG_KEYS.workspaceGid) ?? undefined,
35
+ workspaceName: getSetting(db, ASANA_CONFIG_KEYS.workspaceName) ?? undefined,
36
+ projectGid: getSetting(db, ASANA_CONFIG_KEYS.projectGid) ?? undefined,
37
+ projectName: getSetting(db, ASANA_CONFIG_KEYS.projectName) ?? undefined,
38
+ };
39
+ }
40
+ export function saveAsanaAccessToken(db, accessToken) {
41
+ setSetting(db, ASANA_CONFIG_KEYS.accessToken, accessToken);
42
+ }
43
+ export function saveAsanaWorkspace(db, workspaceGid, workspaceName) {
44
+ setSetting(db, ASANA_CONFIG_KEYS.workspaceGid, workspaceGid);
45
+ setSetting(db, ASANA_CONFIG_KEYS.workspaceName, workspaceName);
46
+ }
47
+ export function saveAsanaProject(db, projectGid, projectName) {
48
+ setSetting(db, ASANA_CONFIG_KEYS.projectGid, projectGid);
49
+ setSetting(db, ASANA_CONFIG_KEYS.projectName, projectName);
50
+ }
51
+ export function clearAsanaConfig(db) {
52
+ for (const key of Object.values(ASANA_CONFIG_KEYS)) {
53
+ deleteSetting(db, key);
54
+ }
55
+ }
56
+ export function getAsanaAccessToken(db) {
57
+ const envToken = process.env.PRLT_ASANA_ACCESS_TOKEN || process.env.ASANA_ACCESS_TOKEN;
58
+ if (envToken)
59
+ return envToken;
60
+ return getSetting(db, ASANA_CONFIG_KEYS.accessToken);
61
+ }
@@ -0,0 +1,5 @@
1
+ export { AsanaClient } from './client.js';
2
+ export { isAsanaConfigured, loadAsanaConfig, saveAsanaAccessToken, saveAsanaWorkspace, saveAsanaProject, clearAsanaConfig, getAsanaAccessToken, } from './config.js';
3
+ export { AsanaMapper } from './mapper.js';
4
+ export { AsanaSync } from './sync.js';
5
+ export type { AsanaConfig, AsanaWorkspace, AsanaProject, AsanaUser, AsanaTask, AsanaTaskMap, AsanaTaskUpsertInput, } from './types.js';
@@ -0,0 +1,4 @@
1
+ export { AsanaClient } from './client.js';
2
+ export { isAsanaConfigured, loadAsanaConfig, saveAsanaAccessToken, saveAsanaWorkspace, saveAsanaProject, clearAsanaConfig, getAsanaAccessToken, } from './config.js';
3
+ export { AsanaMapper } from './mapper.js';
4
+ export { AsanaSync } from './sync.js';
@@ -0,0 +1,13 @@
1
+ import Database from 'better-sqlite3';
2
+ import type { AsanaTaskMap } from './types.js';
3
+ export declare class AsanaMapper {
4
+ private db;
5
+ constructor(db: Database.Database);
6
+ private ensureTable;
7
+ createOrUpdateMapping(pmoTicketId: string, asanaTaskGid: string, asanaProjectGid?: string): void;
8
+ getByTicketId(ticketId: string): AsanaTaskMap | null;
9
+ getByTaskGid(asanaTaskGid: string): AsanaTaskMap | null;
10
+ listMappings(): AsanaTaskMap[];
11
+ updateSyncTimestamp(ticketId: string): void;
12
+ private rowToMap;
13
+ }
@@ -0,0 +1,70 @@
1
+ import { PMO_TABLES } from '../pmo/schema.js';
2
+ export class AsanaMapper {
3
+ db;
4
+ constructor(db) {
5
+ this.db = db;
6
+ this.ensureTable();
7
+ }
8
+ ensureTable() {
9
+ this.db.exec(`
10
+ CREATE TABLE IF NOT EXISTS ${PMO_TABLES.asana_task_map} (
11
+ pmo_ticket_id TEXT NOT NULL REFERENCES ${PMO_TABLES.tickets}(id) ON DELETE CASCADE,
12
+ asana_task_gid TEXT NOT NULL,
13
+ asana_project_gid TEXT,
14
+ last_synced_at TIMESTAMP,
15
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
16
+ PRIMARY KEY (pmo_ticket_id),
17
+ UNIQUE (asana_task_gid)
18
+ )
19
+ `);
20
+ this.db.exec(`
21
+ CREATE INDEX IF NOT EXISTS idx_pmo_asana_task_map_task_gid
22
+ ON ${PMO_TABLES.asana_task_map}(asana_task_gid)
23
+ `);
24
+ }
25
+ createOrUpdateMapping(pmoTicketId, asanaTaskGid, asanaProjectGid) {
26
+ this.db.prepare(`
27
+ INSERT INTO ${PMO_TABLES.asana_task_map}
28
+ (pmo_ticket_id, asana_task_gid, asana_project_gid, last_synced_at, created_at)
29
+ VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
30
+ ON CONFLICT(pmo_ticket_id) DO UPDATE SET
31
+ asana_task_gid = excluded.asana_task_gid,
32
+ asana_project_gid = excluded.asana_project_gid,
33
+ last_synced_at = CURRENT_TIMESTAMP
34
+ `).run(pmoTicketId, asanaTaskGid, asanaProjectGid ?? null);
35
+ }
36
+ getByTicketId(ticketId) {
37
+ const row = this.db.prepare(`
38
+ SELECT * FROM ${PMO_TABLES.asana_task_map} WHERE pmo_ticket_id = ?
39
+ `).get(ticketId);
40
+ return row ? this.rowToMap(row) : null;
41
+ }
42
+ getByTaskGid(asanaTaskGid) {
43
+ const row = this.db.prepare(`
44
+ SELECT * FROM ${PMO_TABLES.asana_task_map} WHERE asana_task_gid = ?
45
+ `).get(asanaTaskGid);
46
+ return row ? this.rowToMap(row) : null;
47
+ }
48
+ listMappings() {
49
+ const rows = this.db.prepare(`
50
+ SELECT * FROM ${PMO_TABLES.asana_task_map} ORDER BY created_at DESC
51
+ `).all();
52
+ return rows.map((row) => this.rowToMap(row));
53
+ }
54
+ updateSyncTimestamp(ticketId) {
55
+ this.db.prepare(`
56
+ UPDATE ${PMO_TABLES.asana_task_map}
57
+ SET last_synced_at = CURRENT_TIMESTAMP
58
+ WHERE pmo_ticket_id = ?
59
+ `).run(ticketId);
60
+ }
61
+ rowToMap(row) {
62
+ return {
63
+ pmoTicketId: row.pmo_ticket_id,
64
+ asanaTaskGid: row.asana_task_gid,
65
+ asanaProjectGid: row.asana_project_gid ?? undefined,
66
+ lastSyncedAt: row.last_synced_at ? new Date(row.last_synced_at) : undefined,
67
+ createdAt: new Date(row.created_at),
68
+ };
69
+ }
70
+ }
@@ -0,0 +1,13 @@
1
+ import type { Ticket } from '../pmo/types.js';
2
+ import { AsanaClient } from './client.js';
3
+ import { AsanaMapper } from './mapper.js';
4
+ export declare class AsanaSync {
5
+ private client;
6
+ private mapper;
7
+ constructor(client: AsanaClient, mapper: AsanaMapper);
8
+ private buildTaskName;
9
+ private buildTaskNotes;
10
+ private isCompletedStatus;
11
+ createTaskForTicket(ticket: Ticket, projectGid: string): Promise<string>;
12
+ syncTicket(ticket: Ticket, taskGid: string): Promise<void>;
13
+ }