From e7e00de109fba6759e0589dbdc6c7e0899296111 Mon Sep 17 00:00:00 2001 From: Kris Braun Date: Sat, 3 Jan 2026 17:12:10 -0500 Subject: [PATCH 1/3] Fix incorrect callback arguments --- tools/gmail/src/gmail.ts | 1 - tools/google-calendar/src/google-calendar.ts | 1 - tools/google-contacts/src/google-contacts.ts | 1 - tools/outlook-calendar/src/outlook-calendar.ts | 1 - tools/slack/src/slack.ts | 1 - twister/src/tool.ts | 2 +- twister/src/tools/ai.ts | 4 ++-- 7 files changed, 3 insertions(+), 8 deletions(-) diff --git a/tools/gmail/src/gmail.ts b/tools/gmail/src/gmail.ts index d68a588..82d193f 100644 --- a/tools/gmail/src/gmail.ts +++ b/tools/gmail/src/gmail.ts @@ -320,7 +320,6 @@ export class Gmail extends Tool implements MessagingTool { } async syncBatch( - _args: any, batchNumber: number, mode: "full" | "incremental", authToken: string, diff --git a/tools/google-calendar/src/google-calendar.ts b/tools/google-calendar/src/google-calendar.ts index d511803..55a880d 100644 --- a/tools/google-calendar/src/google-calendar.ts +++ b/tools/google-calendar/src/google-calendar.ts @@ -347,7 +347,6 @@ export class GoogleCalendar } async syncBatch( - _args: any, batchNumber: number, mode: "full" | "incremental", authToken: string, diff --git a/tools/google-contacts/src/google-contacts.ts b/tools/google-contacts/src/google-contacts.ts index 5fefbe5..21a15de 100644 --- a/tools/google-contacts/src/google-contacts.ts +++ b/tools/google-contacts/src/google-contacts.ts @@ -404,7 +404,6 @@ export default class GoogleContacts } async syncBatch( - _args: any, batchNumber: number, authToken: string ): Promise { diff --git a/tools/outlook-calendar/src/outlook-calendar.ts b/tools/outlook-calendar/src/outlook-calendar.ts index 8f95c2f..1b365fe 100644 --- a/tools/outlook-calendar/src/outlook-calendar.ts +++ b/tools/outlook-calendar/src/outlook-calendar.ts @@ -330,7 +330,6 @@ export class OutlookCalendar } async syncOutlookBatch( - _args: any, calendarId: string, authToken: string, syncMeta: { initialSync: boolean } diff --git a/tools/slack/src/slack.ts b/tools/slack/src/slack.ts index 598d443..34f9611 100644 --- a/tools/slack/src/slack.ts +++ b/tools/slack/src/slack.ts @@ -295,7 +295,6 @@ export class Slack extends Tool implements MessagingTool { } async syncBatch( - _args: any, batchNumber: number, mode: "full" | "incremental", authToken: string, diff --git a/twister/src/tool.ts b/twister/src/tool.ts index 2b009e0..bc44ef4 100644 --- a/twister/src/tool.ts +++ b/twister/src/tool.ts @@ -83,7 +83,7 @@ export abstract class Tool implements ITool { /** * Creates a persistent callback to a method on this tool. * - * ExtraArgs are strongly typed to match the method's signature after the first argument. + * ExtraArgs are strongly typed to match the method's signature. * * @param fn - The method to callback * @param extraArgs - Additional arguments to pass (type-checked, must be serializable) diff --git a/twister/src/tools/ai.ts b/twister/src/tools/ai.ts index 0fd142c..f787ad7 100644 --- a/twister/src/tools/ai.ts +++ b/twister/src/tools/ai.ts @@ -531,9 +531,9 @@ export interface TextPart { } /** - * Data content. Can either be a base64-encoded string, a Uint8Array, an ArrayBuffer, or a Buffer. + * Data content. Can either be a base64-encoded string, a Uint8Array, or an ArrayBuffer. */ -export type DataContent = string | Uint8Array | ArrayBuffer | Buffer; +export type DataContent = string | Uint8Array | ArrayBuffer; /** * Image content part of a prompt. It contains an image. From bc1ccd478fe2ab3f6ac7251172f35dcb98ca4973 Mon Sep 17 00:00:00 2001 From: Kris Braun Date: Sat, 3 Jan 2026 17:18:43 -0500 Subject: [PATCH 2/3] Jira and Asana --- pnpm-lock.yaml | 979 ++++++++++++++++++++++++++++++- tools/asana/package.json | 54 ++ tools/asana/src/asana.ts | 455 ++++++++++++++ tools/asana/src/index.ts | 2 + tools/asana/tsconfig.json | 8 + tools/jira/package.json | 53 ++ tools/jira/src/index.ts | 2 + tools/jira/src/jira.ts | 429 ++++++++++++++ tools/jira/tsconfig.json | 8 + twister/README.md | 3 + twister/docs/CORE_CONCEPTS.md | 59 ++ twister/docs/GETTING_STARTED.md | 33 ++ twister/docs/TOOLS_GUIDE.md | 36 ++ twister/package.json | 2 + twister/src/plot.ts | 73 ++- twister/src/tag.ts | 1 + twists/project-sync/package.json | 6 +- twists/project-sync/src/index.ts | 185 ++++-- 18 files changed, 2328 insertions(+), 60 deletions(-) create mode 100644 tools/asana/package.json create mode 100644 tools/asana/src/asana.ts create mode 100644 tools/asana/src/index.ts create mode 100644 tools/asana/tsconfig.json create mode 100644 tools/jira/package.json create mode 100644 tools/jira/src/index.ts create mode 100644 tools/jira/src/jira.ts create mode 100644 tools/jira/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7264b14..3dd3c3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 0.5.1 '@changesets/cli': specifier: ^2.29.7 - version: 2.29.7(@types/node@24.7.2) + version: 2.29.7(@types/node@25.0.3) '@changesets/get-github-info': specifier: ^0.6.0 version: 0.6.0 @@ -24,6 +24,25 @@ importers: specifier: ^5.9.3 version: 5.9.3 + tools/asana: + dependencies: + '@plotday/twister': + specifier: workspace:^ + version: link:../../twister + asana: + specifier: ^3.0.10 + version: 3.1.5(@babel/core@7.28.5) + devDependencies: + '@types/asana': + specifier: ^0.18.17 + version: 0.18.17 + '@types/node': + specifier: ^25.0.3 + version: 25.0.3 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + tools/gmail: dependencies: '@plotday/twister': @@ -57,6 +76,19 @@ importers: specifier: ^5.9.3 version: 5.9.3 + tools/jira: + dependencies: + '@plotday/twister': + specifier: workspace:^ + version: link:../../twister + jira.js: + specifier: ^4.0.2 + version: 4.1.3 + devDependencies: + typescript: + specifier: ^5.9.3 + version: 5.9.3 + tools/linear: dependencies: '@linear/sdk': @@ -111,6 +143,12 @@ importers: specifier: ^1.0.35 version: 1.0.35 devDependencies: + '@types/asana': + specifier: ^0.18.17 + version: 0.18.17 + '@types/node': + specifier: ^25.0.3 + version: 25.0.3 '@types/prompts': specifier: ^2.4.9 version: 2.4.9 @@ -174,6 +212,12 @@ importers: twists/project-sync: dependencies: + '@plotday/tool-asana': + specifier: workspace:^ + version: link:../../tools/asana + '@plotday/tool-jira': + specifier: workspace:^ + version: link:../../tools/jira '@plotday/tool-linear': specifier: workspace:^ version: link:../../tools/linear @@ -187,10 +231,84 @@ importers: packages: + '@babel/cli@7.28.3': + resolution: {integrity: sha512-n1RU5vuCX0CsaqaXm9I0KUCNKNQMy5epmzl/xdSSm70bSqhg9GWhgeosypyQLc0bK24+Xpk1WGzZlI9pJtkZdg==} + engines: {node: '>=6.9.0'} + hasBin: true + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + '@changesets/apply-release-plan@7.0.13': resolution: {integrity: sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg==} @@ -575,6 +693,22 @@ packages: '@types/node': optional: true + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@linear/sdk@27.0.0': resolution: {integrity: sha512-cortZZnC9VyMfDs2YvBxr7LNi5Ab7zw2eHF6XNuU9dqoEn4nok/oSWIdjjo5zXqIFIEeWmaby3d7WDKdHEEi2g==} engines: {node: '>=12.x', yarn: 1.x} @@ -585,6 +719,9 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': + resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -612,14 +749,20 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@types/asana@0.18.17': + resolution: {integrity: sha512-GWVzchk2y+EyGaIb25ce35XJ/t/OA/utDLbBrtGxGrG7vd2/021GKt/M4oCiWEnNxDr6tuk2PhQBlAMQHmcH3w==} + + '@types/bluebird@3.5.42': + resolution: {integrity: sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@24.7.2': - resolution: {integrity: sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==} + '@types/node@25.0.3': + resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} '@types/prompts@2.4.9': resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} @@ -639,6 +782,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -649,13 +796,33 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + asana@3.1.5: + resolution: {integrity: sha512-MhduaTZhYAEOe0Qk1Wm2HfzYJ0Wss+dtAT9MCUZQQ5q6CnRK+vekSY0F4HKGH6plBnibt4AdVyaHVKPTrGwjVA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} + hasBin: true + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -663,6 +830,22 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001762: + resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -670,6 +853,10 @@ packages: chardet@2.1.0: resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -681,10 +868,30 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -692,6 +899,19 @@ packages: dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -708,6 +928,13 @@ packages: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -716,6 +943,22 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.24.2: resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} engines: {node: '>=18'} @@ -726,6 +969,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -738,6 +985,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -749,6 +999,31 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@3.0.4: + resolution: {integrity: sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==} + engines: {node: '>= 6'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formdata-node@6.0.3: + resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==} + engines: {node: '>= 18'} + + formidable@1.2.6: + resolution: {integrity: sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==} + deprecated: 'Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau' + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -757,11 +1032,32 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fs-readdir-recursive@1.1.0: + resolution: {integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-tsconfig@4.12.0: resolution: {integrity: sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==} @@ -769,10 +1065,18 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -784,6 +1088,18 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + human-id@4.1.2: resolution: {integrity: sha512-v/J+4Z/1eIJovEBdlV5TYj1IR+ZiohcYGRY+qN/oC9dAfKzVT023N/Bgw37hrKCoVRBvk3bqyzpr2PP5YeTMSg==} hasBin: true @@ -801,6 +1117,17 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -827,10 +1154,26 @@ packages: isomorphic-unfetch@3.1.0: resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==} + jira.js@4.1.3: + resolution: {integrity: sha512-I8Co6pihFhIsauxRVEozbItFruu1EJ35fhHTyDjJTdIixY+qD+sSIXDoXlyKJ/Qx7mnNNsDWP3wWu5H8uKHLkw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -848,13 +1191,24 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -862,10 +1216,35 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mime@4.1.0: + resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} + engines: {node: '>=16'} + hasBin: true + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -874,6 +1253,9 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -883,6 +1265,20 @@ packages: encoding: optional: true + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -913,6 +1309,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -941,10 +1341,17 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -955,6 +1362,14 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -969,9 +1384,20 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -985,6 +1411,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -992,6 +1434,10 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slash@2.0.0: + resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} + engines: {node: '>=6'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -1002,6 +1448,9 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1010,6 +1459,11 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + superagent@6.1.0: + resolution: {integrity: sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==} + engines: {node: '>= 7.0.0'} + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1025,6 +1479,9 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.20.6: resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} engines: {node: '>=18.0.0'} @@ -1053,8 +1510,8 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - undici-types@7.14.0: - resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} unfetch@4.2.0: resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==} @@ -1063,6 +1520,15 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -1074,6 +1540,12 @@ packages: engines: {node: '>= 8'} hasBin: true + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.1: resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} engines: {node: '>= 14.6'} @@ -1081,8 +1553,122 @@ packages: snapshots: + '@babel/cli@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@jridgewell/trace-mapping': 0.3.31 + commander: 6.2.1 + convert-source-map: 2.0.0 + fs-readdir-recursive: 1.1.0 + glob: 7.2.3 + make-dir: 2.1.0 + slash: 2.0.0 + optionalDependencies: + '@nicolo-ribaudo/chokidar-2': 2.1.8-no-fsevents.3 + chokidar: 3.6.0 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.5': {} + + '@babel/core@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + '@babel/runtime@7.28.4': {} + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@changesets/apply-release-plan@7.0.13': dependencies: '@changesets/config': 3.1.1 @@ -1120,7 +1706,7 @@ snapshots: transitivePeerDependencies: - encoding - '@changesets/cli@2.29.7(@types/node@24.7.2)': + '@changesets/cli@2.29.7(@types/node@25.0.3)': dependencies: '@changesets/apply-release-plan': 7.0.13 '@changesets/assemble-release-plan': 6.0.9 @@ -1136,7 +1722,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.2(@types/node@24.7.2) + '@inquirer/external-editor': 1.0.2(@types/node@25.0.3) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 ci-info: 3.9.0 @@ -1407,12 +1993,31 @@ snapshots: dependencies: graphql: 15.10.1 - '@inquirer/external-editor@1.0.2(@types/node@24.7.2)': + '@inquirer/external-editor@1.0.2(@types/node@25.0.3)': dependencies: chardet: 2.1.0 iconv-lite: 0.7.0 optionalDependencies: - '@types/node': 24.7.2 + '@types/node': 25.0.3 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 '@linear/sdk@27.0.0': dependencies: @@ -1438,6 +2043,9 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1470,19 +2078,25 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@types/asana@0.18.17': + dependencies: + '@types/bluebird': 3.5.42 + + '@types/bluebird@3.5.42': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 '@types/node@12.20.55': {} - '@types/node@24.7.2': + '@types/node@25.0.3': dependencies: - undici-types: 7.14.0 + undici-types: 7.16.0 '@types/prompts@2.4.9': dependencies: - '@types/node': 24.7.2 + '@types/node': 25.0.3 kleur: 3.0.3 '@types/unist@3.0.3': {} @@ -1495,6 +2109,12 @@ snapshots: dependencies: color-convert: 2.0.1 + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + optional: true + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -1503,12 +2123,40 @@ snapshots: array-union@2.1.0: {} + asana@3.1.5(@babel/core@7.28.5): + dependencies: + '@babel/cli': 7.28.3(@babel/core@7.28.5) + superagent: 6.1.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + asynckit@0.4.0: {} + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + balanced-match@1.0.2: {} + baseline-browser-mapping@2.9.11: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 + binary-extensions@2.3.0: + optional: true + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -1517,6 +2165,26 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001762 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-lite@1.0.30001762: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -1524,6 +2192,19 @@ snapshots: chardet@2.1.0: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + optional: true + ci-info@3.9.0: {} color-convert@2.0.1: @@ -1532,8 +2213,22 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@12.1.0: {} + commander@6.2.1: {} + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cookiejar@2.1.4: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -1542,6 +2237,12 @@ snapshots: dataloader@1.4.0: {} + debug@4.4.3: + dependencies: + ms: 2.1.3 + + delayed-stream@1.0.0: {} + detect-indent@6.1.0: {} dir-glob@3.0.1: @@ -1552,6 +2253,14 @@ snapshots: dotenv@8.6.0: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.267: {} + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -1559,6 +2268,21 @@ snapshots: entities@4.5.0: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.24.2: optionalDependencies: '@esbuild/aix-ppc64': 0.24.2 @@ -1616,6 +2340,8 @@ snapshots: '@esbuild/win32-ia32': 0.25.11 '@esbuild/win32-x64': 0.25.11 + escalade@3.2.0: {} + esprima@4.0.1: {} extendable-error@0.1.7: {} @@ -1628,6 +2354,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-safe-stringify@2.1.1: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -1641,6 +2369,28 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + follow-redirects@1.15.11: {} + + form-data@3.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formdata-node@6.0.3: {} + + formidable@1.2.6: {} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -1653,9 +2403,35 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fs-readdir-recursive@1.1.0: {} + + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-tsconfig@4.12.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -1664,6 +2440,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -1673,12 +2458,24 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} graphql@15.10.1: {} has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + human-id@4.1.2: {} husky@9.1.7: {} @@ -1689,6 +2486,18 @@ snapshots: ignore@5.3.2: {} + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + optional: true + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -1712,11 +2521,26 @@ snapshots: transitivePeerDependencies: - encoding + jira.js@4.1.3: + dependencies: + axios: 1.13.2 + formdata-node: 6.0.3 + mime: 4.1.0 + tslib: 2.8.1 + transitivePeerDependencies: + - debug + + js-tokens@4.0.0: {} + js-yaml@3.14.1: dependencies: argparse: 1.0.10 esprima: 4.0.1 + jsesc@3.1.0: {} + + json5@2.2.3: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -1733,8 +2557,17 @@ snapshots: lodash.startcase@4.4.0: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + lunr@2.3.9: {} + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + markdown-it@14.1.0: dependencies: argparse: 2.0.1 @@ -1744,25 +2577,56 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + math-intrinsics@1.1.0: {} + mdurl@2.0.0: {} merge2@1.4.1: {} + methods@1.1.2: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@2.6.0: {} + + mime@4.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 mri@1.2.0: {} + ms@2.1.3: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 + node-releases@2.0.27: {} + + normalize-path@3.0.0: + optional: true + + object-inspect@1.13.4: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + outdent@0.5.0: {} p-filter@2.1.0: @@ -1787,6 +2651,8 @@ snapshots: path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-type@4.0.0: {} @@ -1804,8 +2670,14 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + proxy-from-env@1.1.0: {} + punycode.js@2.3.1: {} + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -1817,6 +2689,17 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + optional: true + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -1827,8 +2710,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} + semver@5.7.2: {} + + semver@6.3.1: {} + semver@7.7.3: {} shebang-command@2.0.0: @@ -1837,10 +2726,40 @@ snapshots: shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + signal-exit@4.1.0: {} sisteransi@1.0.5: {} + slash@2.0.0: {} + slash@3.0.0: {} spawndamnit@3.0.1: @@ -1850,12 +2769,32 @@ snapshots: sprintf-js@1.0.3: {} + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 strip-bom@3.0.0: {} + superagent@6.1.0: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3 + fast-safe-stringify: 2.1.1 + form-data: 3.0.4 + formidable: 1.2.6 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.14.1 + readable-stream: 3.6.2 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -1868,6 +2807,8 @@ snapshots: tr46@0.0.3: {} + tslib@2.8.1: {} + tsx@4.20.6: dependencies: esbuild: 0.25.11 @@ -1894,12 +2835,20 @@ snapshots: uc.micro@2.1.0: {} - undici-types@7.14.0: {} + undici-types@7.16.0: {} unfetch@4.2.0: {} universalify@0.1.2: {} + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + webidl-conversions@3.0.1: {} whatwg-url@5.0.0: @@ -1911,4 +2860,8 @@ snapshots: dependencies: isexe: 2.0.0 + wrappy@1.0.2: {} + + yallist@3.1.1: {} + yaml@2.8.1: {} diff --git a/tools/asana/package.json b/tools/asana/package.json new file mode 100644 index 0000000..05fe33a --- /dev/null +++ b/tools/asana/package.json @@ -0,0 +1,54 @@ +{ + "name": "@plotday/tool-asana", + "displayName": "Asana", + "description": "Sync with Asana project management", + "author": "Plot (https://plot.day)", + "license": "MIT", + "version": "0.1.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "@plotday/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist" + }, + "dependencies": { + "@plotday/twister": "workspace:^", + "asana": "^3.0.10" + }, + "devDependencies": { + "@types/asana": "^0.18.17", + "@types/node": "^25.0.3", + "typescript": "^5.9.3" + }, + "repository": { + "type": "git", + "url": "https://github.com/plotday/plot.git", + "directory": "tools/asana" + }, + "homepage": "https://plot.day", + "bugs": { + "url": "https://github.com/plotday/plot/issues" + }, + "keywords": [ + "plot", + "tool", + "asana", + "project-management" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/tools/asana/src/asana.ts b/tools/asana/src/asana.ts new file mode 100644 index 0000000..22c1b31 --- /dev/null +++ b/tools/asana/src/asana.ts @@ -0,0 +1,455 @@ +import * as asana from "asana"; + +import { + type ActivityLink, + ActivityType, + type NewActivityWithNotes, +} from "@plotday/twister"; +import type { + Project, + ProjectAuth, + ProjectSyncOptions, + ProjectTool, +} from "@plotday/twister/common/projects"; +import { Tool, type ToolBuilder } from "@plotday/twister/tool"; +import { type Callback, Callbacks } from "@plotday/twister/tools/callbacks"; +import { + AuthLevel, + AuthProvider, + type Authorization, + Integrations, +} from "@plotday/twister/tools/integrations"; +import { Network, type WebhookRequest } from "@plotday/twister/tools/network"; +import { ContactAccess, Plot } from "@plotday/twister/tools/plot"; +import { Tasks } from "@plotday/twister/tools/tasks"; + +type SyncState = { + offset: number; + batchNumber: number; + tasksProcessed: number; +}; + +/** + * Asana project management tool + * + * Implements the ProjectTool interface for syncing Asana projects and tasks + * with Plot activities. + */ +export class Asana extends Tool implements ProjectTool { + build(build: ToolBuilder) { + return { + integrations: build(Integrations), + network: build(Network, { urls: ["https://app.asana.com/*"] }), + callbacks: build(Callbacks), + tasks: build(Tasks), + plot: build(Plot, { contact: { access: ContactAccess.Write } }), + }; + } + + /** + * Create Asana API client with auth token + */ + private async getClient(authToken: string): Promise { + const authorization = await this.get( + `authorization:${authToken}` + ); + if (!authorization) { + throw new Error("Authorization no longer available"); + } + + const token = await this.tools.integrations.get(authorization); + if (!token) { + throw new Error("Authorization no longer available"); + } + + return asana.Client.create().useAccessToken(token.token); + } + + /** + * Request Asana OAuth authorization + */ + async requestAuth< + TCallback extends (auth: ProjectAuth, ...args: any[]) => any + >( + callback: TCallback, + ...extraArgs: TCallback extends (auth: any, ...rest: infer R) => any + ? R + : [] + ): Promise { + const asanaScopes = ["default"]; + + // Generate opaque token for authorization + const authToken = crypto.randomUUID(); + + const callbackToken = await this.tools.callbacks.createFromParent( + callback, + ...extraArgs + ); + + // Request auth and return the activity link + return await this.tools.integrations.request( + { + provider: AuthProvider.Asana, + level: AuthLevel.User, + scopes: asanaScopes, + }, + this.onAuthSuccess, + authToken, + callbackToken + ); + } + + /** + * Handle successful OAuth authorization + */ + private async onAuthSuccess( + authorization: Authorization, + authToken: string, + callbackToken: Callback + ): Promise { + // Store authorization for later use + await this.set(`authorization:${authToken}`, authorization); + + // Execute the callback with the auth token + await this.run(callbackToken, { authToken }); + } + + /** + * Get list of Asana projects + */ + async getProjects(authToken: string): Promise { + const client = await this.getClient(authToken); + + // Get user's workspaces first + const workspaces = await client.workspaces.getWorkspaces(); + + const allProjects: Project[] = []; + + // Get projects from each workspace + for (const workspace of workspaces.data) { + const projects = await client.projects.findByWorkspace(workspace.gid, { + limit: 100, + }); + + for (const project of projects.data) { + allProjects.push({ + id: project.gid, + name: project.name, + description: null, // Asana doesn't return description in list + key: null, // Asana doesn't have project keys + }); + } + } + + return allProjects; + } + + /** + * Start syncing tasks from an Asana project + */ + async startSync< + TCallback extends (task: NewActivityWithNotes, ...args: any[]) => any + >( + authToken: string, + projectId: string, + callback: TCallback, + options?: ProjectSyncOptions, + ...extraArgs: TCallback extends (task: any, ...rest: infer R) => any + ? R + : [] + ): Promise { + // Setup webhook for real-time updates + await this.setupAsanaWebhook(authToken, projectId); + + // Store callback for webhook processing + const callbackToken = await this.tools.callbacks.createFromParent( + callback, + ...extraArgs + ); + await this.set(`callback_${projectId}`, callbackToken); + + // Start initial batch sync + await this.startBatchSync(authToken, projectId, options); + } + + /** + * Setup Asana webhook for real-time updates + * Note: Asana webhook API requires special permissions, so we skip for now + */ + private async setupAsanaWebhook( + authToken: string, + projectId: string + ): Promise { + // TODO: Implement Asana webhooks once we confirm the correct API + // The asana SDK webhook API may require special app permissions + console.log(`Asana webhooks not yet implemented for project ${projectId}`); + } + + /** + * Initialize batch sync process + */ + private async startBatchSync( + authToken: string, + projectId: string, + options?: ProjectSyncOptions + ): Promise { + // Initialize sync state + await this.set(`sync_state_${projectId}`, { + offset: 0, + batchNumber: 1, + tasksProcessed: 0, + }); + + // Queue first batch + const batchCallback = await this.callback( + this.syncBatch, + authToken, + projectId, + options + ); + + await this.tools.tasks.runTask(batchCallback); + } + + /** + * Process a batch of tasks + */ + private async syncBatch( + authToken: string, + projectId: string, + options?: ProjectSyncOptions + ): Promise { + const state = await this.get(`sync_state_${projectId}`); + if (!state) { + throw new Error(`Sync state not found for project ${projectId}`); + } + + // Retrieve callback token from storage + const callbackToken = await this.get(`callback_${projectId}`); + if (!callbackToken) { + throw new Error(`Callback token not found for project ${projectId}`); + } + + const client = await this.getClient(authToken); + + // Build request params + const batchSize = 50; + const params: any = { + project: projectId, + limit: batchSize, + opt_fields: [ + "name", + "notes", + "completed", + "completed_at", + "created_at", + "modified_at", + ].join(","), + }; + + if (state.offset > 0) { + params.offset = state.offset; + } + + // Fetch batch of tasks using findAll + const tasksResult = await client.tasks.findAll(params); + + // Process each task + for (const task of tasksResult.data) { + // Optionally filter by time + if (options?.timeMin) { + const createdAt = new Date(task.created_at); + if (createdAt < options.timeMin) { + continue; + } + } + + const activityWithNotes = await this.convertTaskToActivity( + task, + projectId + ); + // Execute the callback using the callback token + await this.tools.callbacks.run(callbackToken, activityWithNotes); + } + + // Check if more pages by checking if we got a full batch + const hasMore = tasksResult.data.length === batchSize; + + if (hasMore) { + await this.set(`sync_state_${projectId}`, { + offset: state.offset + batchSize, + batchNumber: state.batchNumber + 1, + tasksProcessed: state.tasksProcessed + tasksResult.data.length, + }); + + // Queue next batch + const nextBatch = await this.callback( + this.syncBatch, + authToken, + projectId, + options + ); + await this.tools.tasks.runTask(nextBatch); + } else { + // Cleanup sync state + await this.clear(`sync_state_${projectId}`); + } + } + + /** + * Convert an Asana task to a Plot Activity + */ + private async convertTaskToActivity( + task: any, + projectId: string + ): Promise { + // Build notes array: description + const notes: Array<{ content: string }> = []; + + if (task.notes) { + notes.push({ content: task.notes }); + } + + // Ensure at least one note exists + if (notes.length === 0) { + notes.push({ content: "" }); + } + + return { + type: ActivityType.Action, + title: task.name, + source: `asana:task:${projectId}:${task.gid}`, + doneAt: task.completed ? new Date(task.completed_at || Date.now()) : null, + notes, + }; + } + + /** + * Verify Asana webhook signature + * Asana uses HMAC-SHA256 with a shared secret + */ + private async verifyAsanaSignature( + signature: string | undefined, + rawBody: string, + secret: string + ): Promise { + if (!signature) { + console.warn("Asana webhook missing signature header"); + return false; + } + + // Compute HMAC-SHA256 signature + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + "raw", + encoder.encode(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"] + ); + const signatureBytes = await crypto.subtle.sign( + "HMAC", + key, + encoder.encode(rawBody) + ); + + // Convert to hex string + const expectedSignature = Array.from(new Uint8Array(signatureBytes)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + + // Constant-time comparison + return signature === expectedSignature; + } + + /** + * Handle incoming webhook events from Asana + */ + private async onWebhook( + request: WebhookRequest, + projectId: string, + authToken: string + ): Promise { + const payload = request.body as any; + + // Asana webhook handshake + if (request.headers["x-hook-secret"]) { + // This is the initial handshake, respond with the secret + // Note: The network tool should handle this automatically + return; + } + + // Verify webhook signature + const webhookId = await this.get(`webhook_id_${projectId}`); + if (webhookId && request.rawBody) { + const signature = request.headers["x-hook-signature"]; + // For Asana, the secret is the webhook ID itself + const isValid = await this.verifyAsanaSignature( + signature, + request.rawBody, + webhookId + ); + + if (!isValid) { + console.warn("Asana webhook signature verification failed"); + return; + } + } + + // Process events + if (payload.events && Array.isArray(payload.events)) { + const callbackToken = await this.get(`callback_${projectId}`); + if (!callbackToken) return; + + const client = await this.getClient(authToken); + + for (const event of payload.events) { + if (event.resource?.resource_type === "task") { + try { + // Fetch full task details + const task = await client.tasks.getTask(event.resource.gid, { + opt_fields: [ + "name", + "notes", + "completed", + "completed_at", + "created_at", + "modified_at", + ].join(","), + }); + + const activityWithNotes = await this.convertTaskToActivity( + task, + projectId + ); + + // Execute stored callback + await this.run(callbackToken, activityWithNotes); + } catch (error) { + console.warn("Failed to process Asana task webhook:", error); + } + } + } + } + } + + /** + * Stop syncing an Asana project + */ + async stopSync(authToken: string, projectId: string): Promise { + // TODO: Remove webhook when webhook support is implemented + await this.clear(`webhook_id_${projectId}`); + + // Cleanup callback + const callbackToken = await this.get(`callback_${projectId}`); + if (callbackToken) { + await this.deleteCallback(callbackToken); + await this.clear(`callback_${projectId}`); + } + + // Cleanup sync state + await this.clear(`sync_state_${projectId}`); + } +} + +export default Asana; diff --git a/tools/asana/src/index.ts b/tools/asana/src/index.ts new file mode 100644 index 0000000..345c247 --- /dev/null +++ b/tools/asana/src/index.ts @@ -0,0 +1,2 @@ +export { Asana } from "./asana"; +export { default } from "./asana"; diff --git a/tools/asana/tsconfig.json b/tools/asana/tsconfig.json new file mode 100644 index 0000000..b98a116 --- /dev/null +++ b/tools/asana/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@plotday/twister/tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/tools/jira/package.json b/tools/jira/package.json new file mode 100644 index 0000000..cfa8fa7 --- /dev/null +++ b/tools/jira/package.json @@ -0,0 +1,53 @@ +{ + "name": "@plotday/tool-jira", + "displayName": "Jira", + "description": "Sync with Jira project management", + "author": "Plot (https://plot.day)", + "license": "MIT", + "version": "0.1.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "@plotday/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist" + }, + "dependencies": { + "@plotday/twister": "workspace:^", + "jira.js": "^4.0.2" + }, + "devDependencies": { + "typescript": "^5.9.3" + }, + "repository": { + "type": "git", + "url": "https://github.com/plotday/plot.git", + "directory": "tools/jira" + }, + "homepage": "https://plot.day", + "bugs": { + "url": "https://github.com/plotday/plot/issues" + }, + "keywords": [ + "plot", + "tool", + "jira", + "atlassian", + "project-management" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/tools/jira/src/index.ts b/tools/jira/src/index.ts new file mode 100644 index 0000000..3bf47bf --- /dev/null +++ b/tools/jira/src/index.ts @@ -0,0 +1,2 @@ +export { Jira } from "./jira"; +export { default } from "./jira"; diff --git a/tools/jira/src/jira.ts b/tools/jira/src/jira.ts new file mode 100644 index 0000000..a1838b5 --- /dev/null +++ b/tools/jira/src/jira.ts @@ -0,0 +1,429 @@ +import { Version3Client } from "jira.js"; + +import { + type ActivityLink, + ActivityType, + type NewActivityWithNotes, +} from "@plotday/twister"; +import type { + Project, + ProjectAuth, + ProjectSyncOptions, + ProjectTool, +} from "@plotday/twister/common/projects"; +import { Tool, type ToolBuilder } from "@plotday/twister/tool"; +import { type Callback, Callbacks } from "@plotday/twister/tools/callbacks"; +import { + AuthLevel, + AuthProvider, + type Authorization, + Integrations, +} from "@plotday/twister/tools/integrations"; +import { Network, type WebhookRequest } from "@plotday/twister/tools/network"; +import { ContactAccess, Plot } from "@plotday/twister/tools/plot"; +import { Tasks } from "@plotday/twister/tools/tasks"; + +type SyncState = { + startAt: number; + batchNumber: number; + issuesProcessed: number; +}; + +/** + * Jira project management tool + * + * Implements the ProjectTool interface for syncing Jira projects and issues + * with Plot activities. + */ +export class Jira extends Tool implements ProjectTool { + build(build: ToolBuilder) { + return { + integrations: build(Integrations), + network: build(Network, { urls: ["https://*.atlassian.net/*"] }), + callbacks: build(Callbacks), + tasks: build(Tasks), + plot: build(Plot, { contact: { access: ContactAccess.Write } }), + }; + } + + /** + * Create Jira API client with auth token + */ + private async getClient(authToken: string): Promise { + const authorization = await this.get( + `authorization:${authToken}` + ); + if (!authorization) { + throw new Error("Authorization no longer available"); + } + + const token = await this.tools.integrations.get(authorization); + if (!token) { + throw new Error("Authorization no longer available"); + } + + // Get the cloud ID from provider metadata + const cloudId = token.provider?.cloud_id; + if (!cloudId) { + throw new Error("Jira cloud ID not found in authorization"); + } + + return new Version3Client({ + host: `https://api.atlassian.com/ex/jira/${cloudId}`, + authentication: { + oauth2: { + accessToken: token.token, + }, + }, + }); + } + + /** + * Request Jira OAuth authorization + */ + async requestAuth< + TCallback extends (auth: ProjectAuth, ...args: any[]) => any + >( + callback: TCallback, + ...extraArgs: TCallback extends (auth: any, ...rest: infer R) => any + ? R + : [] + ): Promise { + const jiraScopes = [ + "read:jira-work", + "write:jira-work", + "read:jira-user", + ]; + + // Generate opaque token for authorization + const authToken = crypto.randomUUID(); + + const callbackToken = await this.tools.callbacks.createFromParent( + callback, + ...extraArgs + ); + + // Request auth and return the activity link + return await this.tools.integrations.request( + { + provider: AuthProvider.Atlassian, + level: AuthLevel.User, + scopes: jiraScopes, + }, + this.onAuthSuccess, + authToken, + callbackToken + ); + } + + /** + * Handle successful OAuth authorization + */ + private async onAuthSuccess( + authorization: Authorization, + authToken: string, + callbackToken: Callback + ): Promise { + // Store authorization for later use + await this.set(`authorization:${authToken}`, authorization); + + // Execute the callback with the auth token + await this.run(callbackToken, { authToken }); + } + + /** + * Get list of Jira projects + */ + async getProjects(authToken: string): Promise { + const client = await this.getClient(authToken); + + // Get all projects the user has access to + const projects = await client.projects.searchProjects({ + maxResults: 100, + }); + + return (projects.values || []).map((project) => ({ + id: project.id, + name: project.name, + description: project.description || null, + key: project.key, + })); + } + + /** + * Start syncing issues from a Jira project + */ + async startSync< + TCallback extends (issue: NewActivityWithNotes, ...args: any[]) => any + >( + authToken: string, + projectId: string, + callback: TCallback, + options?: ProjectSyncOptions, + ...extraArgs: TCallback extends (issue: any, ...rest: infer R) => any + ? R + : [] + ): Promise { + // Setup webhook for real-time updates + await this.setupJiraWebhook(authToken, projectId); + + // Store callback for webhook processing + const callbackToken = await this.tools.callbacks.createFromParent( + callback, + ...extraArgs + ); + await this.set(`callback_${projectId}`, callbackToken); + + // Start initial batch sync + await this.startBatchSync(authToken, projectId, options); + } + + /** + * Setup Jira webhook for real-time updates + * Note: Webhook API varies by Jira version, so we skip for now + */ + private async setupJiraWebhook( + authToken: string, + projectId: string + ): Promise { + // TODO: Implement Jira webhooks once we confirm the correct API + // The jira.js library webhook API may vary by Jira version + console.log(`Jira webhooks not yet implemented for project ${projectId}`); + } + + /** + * Initialize batch sync process + */ + private async startBatchSync( + authToken: string, + projectId: string, + options?: ProjectSyncOptions + ): Promise { + // Initialize sync state + await this.set(`sync_state_${projectId}`, { + startAt: 0, + batchNumber: 1, + issuesProcessed: 0, + }); + + // Queue first batch + const batchCallback = await this.callback( + this.syncBatch, + authToken, + projectId, + options + ); + + await this.tools.tasks.runTask(batchCallback); + } + + /** + * Process a batch of issues + */ + private async syncBatch( + authToken: string, + projectId: string, + options?: ProjectSyncOptions + ): Promise { + const state = await this.get(`sync_state_${projectId}`); + if (!state) { + throw new Error(`Sync state not found for project ${projectId}`); + } + + // Retrieve callback token from storage + const callbackToken = await this.get(`callback_${projectId}`); + if (!callbackToken) { + throw new Error(`Callback token not found for project ${projectId}`); + } + + const client = await this.getClient(authToken); + + // Build JQL query + let jql = `project = ${projectId}`; + if (options?.timeMin) { + const timeMinStr = options.timeMin.toISOString().split("T")[0]; + jql += ` AND created >= "${timeMinStr}"`; + } + jql += ` ORDER BY created ASC`; + + // Fetch batch of issues (50 at a time) + const batchSize = 50; + const searchResult = await client.issueSearch.searchForIssuesUsingJql({ + jql, + startAt: state.startAt, + maxResults: batchSize, + fields: [ + "summary", + "description", + "status", + "assignee", + "comment", + "created", + "updated", + ], + }); + + // Process each issue + for (const issue of searchResult.issues || []) { + const activityWithNotes = await this.convertIssueToActivity( + issue, + projectId + ); + // Execute the callback using the callback token + await this.tools.callbacks.run(callbackToken, activityWithNotes); + } + + // Check if more pages + const totalIssues = searchResult.total || 0; + const nextStartAt = state.startAt + batchSize; + + if (nextStartAt < totalIssues) { + await this.set(`sync_state_${projectId}`, { + startAt: nextStartAt, + batchNumber: state.batchNumber + 1, + issuesProcessed: state.issuesProcessed + (searchResult.issues?.length || 0), + }); + + // Queue next batch + const nextBatch = await this.callback( + this.syncBatch, + authToken, + projectId, + options + ); + await this.tools.tasks.runTask(nextBatch); + } else { + // Cleanup sync state + await this.clear(`sync_state_${projectId}`); + } + } + + /** + * Convert a Jira issue to a Plot Activity + */ + private async convertIssueToActivity( + issue: any, + projectId: string + ): Promise { + const fields = issue.fields || {}; + const status = fields.status?.name; + const comments = fields.comment?.comments || []; + + // Build notes array: description + comments + const notes: Array<{ content: string }> = []; + + if (fields.description) { + // Jira uses Atlassian Document Format (ADF), need to convert to plain text + const description = + typeof fields.description === "string" + ? fields.description + : this.extractTextFromADF(fields.description); + notes.push({ content: description }); + } + + for (const comment of comments) { + const commentText = + typeof comment.body === "string" + ? comment.body + : this.extractTextFromADF(comment.body); + notes.push({ content: commentText }); + } + + // Ensure at least one note exists + if (notes.length === 0) { + notes.push({ content: "" }); + } + + return { + type: ActivityType.Action, + title: fields.summary || issue.key, + source: `jira:issue:${projectId}:${issue.key}`, + doneAt: + status === "Done" || status === "Closed" || status === "Resolved" + ? new Date() + : null, + notes, + }; + } + + /** + * Extract plain text from Atlassian Document Format (ADF) + */ + private extractTextFromADF(adf: any): string { + if (!adf || typeof adf !== "object") { + return ""; + } + + let text = ""; + + const traverse = (node: any) => { + if (node.type === "text") { + text += node.text || ""; + } + + if (node.content && Array.isArray(node.content)) { + for (const child of node.content) { + traverse(child); + } + // Add newline after paragraphs + if (node.type === "paragraph") { + text += "\n"; + } + } + }; + + traverse(adf); + return text.trim(); + } + + /** + * Handle incoming webhook events from Jira + */ + private async onWebhook( + request: WebhookRequest, + projectId: string, + authToken: string + ): Promise { + const payload = request.body as any; + + // Jira webhook events have different structure + if ( + payload.webhookEvent?.startsWith("jira:issue_") || + payload.webhookEvent?.startsWith("comment_") + ) { + const callbackToken = await this.get(`callback_${projectId}`); + if (!callbackToken) return; + + const issue = payload.issue; + if (!issue) return; + + const activityWithNotes = await this.convertIssueToActivity( + issue, + projectId + ); + + // Execute stored callback + await this.run(callbackToken, activityWithNotes); + } + } + + /** + * Stop syncing a Jira project + */ + async stopSync(authToken: string, projectId: string): Promise { + // TODO: Remove webhook when webhook support is implemented + await this.clear(`webhook_id_${projectId}`); + + // Cleanup callback + const callbackToken = await this.get(`callback_${projectId}`); + if (callbackToken) { + await this.deleteCallback(callbackToken); + await this.clear(`callback_${projectId}`); + } + + // Cleanup sync state + await this.clear(`sync_state_${projectId}`); + } +} + +export default Jira; diff --git a/tools/jira/tsconfig.json b/tools/jira/tsconfig.json new file mode 100644 index 0000000..b98a116 --- /dev/null +++ b/tools/jira/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@plotday/twister/tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/twister/README.md b/twister/README.md index ab48f69..3ed0098 100644 --- a/twister/README.md +++ b/twister/README.md @@ -136,11 +136,14 @@ Twist tools provide capabilities to twists. They are usually unopinionated and d Think of an **Activity as a thread** and **Notes as messages in that thread**. Always create activities with an initial note, and add notes for updates rather than creating new activities. +**Action scheduling:** When creating Actions (tasks), omitting the `start` field defaults to "Do Now" (current time). For most integrations, explicitly set `start: null` to create backlog items ("Do Someday"), only using "Do Now" for urgent or in-progress tasks. + ```typescript // Create an activity with an initial note (thread with first message) await this.tools.plot.createActivity({ type: ActivityType.Action, title: "Review pull request", + start: null, // "Do Someday" - backlog item (recommended default) source: "github:pr:123", // For deduplication notes: [ { diff --git a/twister/docs/CORE_CONCEPTS.md b/twister/docs/CORE_CONCEPTS.md index 0b68457..c57b0c3 100644 --- a/twister/docs/CORE_CONCEPTS.md +++ b/twister/docs/CORE_CONCEPTS.md @@ -250,6 +250,65 @@ await this.tools.plot.createActivity({ }); ``` +### Activity Scheduling States + +When creating Activities of type `Action` (tasks), the `start` field determines how they appear in Plot: + +- **"Do Now"** (Current/Actionable) - Tasks that should be done today +- **"Do Later"** (Future Scheduled) - Tasks scheduled for a specific future date +- **"Do Someday"** (Unscheduled Backlog) - Tasks without a specific timeline + +#### Default Behavior + +**Important:** When creating an Action, omitting the `start` field defaults to the current time, making it a "Do Now" task. + +For most integrations (project management tools, issue trackers), you should explicitly set `start: null` to create backlog items, only using "Do Now" for tasks that are actively in progress or urgent. + +```typescript +// "Do Now" - Appears in today's actionable list +// WARNING: This is the default when start is omitted! +await this.tools.plot.createActivity({ + type: ActivityType.Action, + title: "Urgent: Review security PR", + // Omitting start defaults to new Date() +}); + +// "Do Someday" - Backlog item (RECOMMENDED for most synced tasks) +await this.tools.plot.createActivity({ + type: ActivityType.Action, + title: "Refactor authentication service", + start: null, // Explicitly set to null for backlog +}); + +// "Do Later" - Scheduled for specific date +await this.tools.plot.createActivity({ + type: ActivityType.Action, + title: "Prepare Q1 review", + start: new Date("2025-03-15"), // Scheduled for future date +}); +``` + +#### When to Use Each State + +**Use "Do Now" (omit `start`)** when: + +- Task is actively being worked on +- Task has a due date of today +- Task is marked as "In Progress" in source system +- Task is high priority AND explicitly assigned as current work + +**Use "Do Someday" (`start: null`)** when: + +- Syncing backlog items from project management tools +- Task is in "To Do" or "Backlog" status +- Task doesn't have a specific due date +- This should be the **default for most integrations** + +**Use "Do Later" (future `start`)** when: + +- Task has a specific due date in the future +- Task is scheduled for a particular day + ### Activity Properties ```typescript diff --git a/twister/docs/GETTING_STARTED.md b/twister/docs/GETTING_STARTED.md index c427f45..cdf9f3e 100644 --- a/twister/docs/GETTING_STARTED.md +++ b/twister/docs/GETTING_STARTED.md @@ -248,6 +248,39 @@ await this.tools.plot.createActivity({ }); ``` +#### Scheduling States for Actions + +**Important:** When creating Actions (tasks), the `start` field determines how they appear in Plot. By default, omitting `start` creates a "Do Now" task. For most integrations, you should explicitly set `start: null` to create backlog items. + +```typescript +// "Do Now" - Actionable today (DEFAULT when start is omitted) +// Use for urgent tasks or items actively in progress +await this.tools.plot.createActivity({ + type: ActivityType.Action, + title: "Fix critical bug in production", + notes: [{ content: "Users reporting login failures" }], + // Omitting start defaults to current time +}); + +// "Do Someday" - Backlog item (RECOMMENDED for most synced tasks) +// Use for task backlog, future ideas, non-urgent items +await this.tools.plot.createActivity({ + type: ActivityType.Action, + title: "Refactor authentication module", + start: null, // Explicitly set to null for backlog + notes: [{ content: "Technical debt item to address later" }], +}); + +// "Do Later" - Scheduled for specific date +// Use when task has a specific due date +await this.tools.plot.createActivity({ + type: ActivityType.Action, + title: "Submit expense report", + start: new Date("2025-01-31"), // Due date + notes: [{ content: "December expenses need to be submitted by end of month" }], +}); +``` + ### Storing Data ```typescript diff --git a/twister/docs/TOOLS_GUIDE.md b/twister/docs/TOOLS_GUIDE.md index e00b151..9d1275c 100644 --- a/twister/docs/TOOLS_GUIDE.md +++ b/twister/docs/TOOLS_GUIDE.md @@ -91,6 +91,42 @@ await this.tools.plot.createActivity({ }); ``` +#### Action Scheduling States + +When creating Actions (tasks), the `start` field determines their scheduling state. **Important:** Omitting `start` defaults to "Do Now" (current time). For most integrations, explicitly set `start: null` to create backlog items. + +```typescript +// "Do Now" - Actionable today (DEFAULT - use sparingly!) +// Only use for tasks that are urgent or actively in progress +await this.tools.plot.createActivity({ + type: ActivityType.Action, + title: "Fix production bug", + notes: [{ content: "Critical issue affecting users" }], + // Omitting start defaults to new Date() - becomes "Do Now" +}); + +// "Do Someday" - Backlog item (RECOMMENDED default for most synced tasks) +// Use for tasks from project management tools, backlog items, future work +await this.tools.plot.createActivity({ + type: ActivityType.Action, + title: "Refactor user service", + start: null, // Explicitly null for backlog + source: "linear:issue:ABC-123", + notes: [{ content: "Technical debt to address" }], +}); + +// "Do Later" - Scheduled for specific date +// Use when task has a concrete due date +await this.tools.plot.createActivity({ + type: ActivityType.Action, + title: "Submit quarterly report", + start: new Date("2025-03-31"), // Due date + notes: [{ content: "Q1 report due end of March" }], +}); +``` + +**Best practice for integrations:** Default to `start: null` for synced tasks, and only set `start` to current time if the task is explicitly marked as current/in-progress in the source system. + ### Updating Activities ```typescript diff --git a/twister/package.json b/twister/package.json index 3151cfa..571d431 100644 --- a/twister/package.json +++ b/twister/package.json @@ -221,6 +221,8 @@ "typebox": "^1.0.35" }, "devDependencies": { + "@types/asana": "^0.18.17", + "@types/node": "^25.0.3", "@types/prompts": "^2.4.9", "tsx": "^4.19.2", "typedoc": "^0.28.14", diff --git a/twister/src/plot.ts b/twister/src/plot.ts index 2d26211..b0b0beb 100644 --- a/twister/src/plot.ts +++ b/twister/src/plot.ts @@ -341,7 +341,31 @@ export type Activity = ActivityCommon & { * Start time of a scheduled activity. Notes are not typically scheduled unless they're about specific times. * For recurring events, this represents the start of the first occurrence. * Can be a Date object for timed events or a date string in "YYYY-MM-DD" format for all-day events. - * Null for activities without scheduled start times. + * + * **Activity Scheduling States** (for Actions): + * - **Do Now** (current/actionable): When creating a NewActivity of type Action, omitting `start` defaults to current time + * - **Do Later** (future scheduled): Set `start` to a future Date or date string + * - **Do Someday** (unscheduled backlog): Explicitly set `start: null` + * + * @example + * ```typescript + * // "Do Now" - defaults to current time when start is omitted + * await this.tools.plot.createActivity({ type: ActivityType.Action, title: "Urgent task" }); + * + * // "Do Later" - scheduled for a specific time + * await this.tools.plot.createActivity({ + * type: ActivityType.Action, + * title: "Future task", + * start: new Date("2025-02-01") + * }); + * + * // "Do Someday" - unscheduled backlog item + * await this.tools.plot.createActivity({ + * type: ActivityType.Action, + * title: "Backlog task", + * start: null + * }); + * ``` */ start: Date | string | null; /** @@ -447,6 +471,16 @@ export type PickPriorityConfig = { * The ID and author will be automatically assigned by the Plot system * based on the current execution context. * + * **Important: Scheduling Defaults for Actions** + * + * When creating an Activity of type `Action`, the `start` field determines its scheduling state: + * - **Omit `start`** → Defaults to current time → "Do Now" (appears in today's actionable list) + * - **Set `start: null`** → Unscheduled → "Do Someday" (backlog item, no specific time) + * - **Set `start` to future Date** → Scheduled → "Do Later" (appears on that date) + * + * For most external task integrations (project management, issue trackers), use `start: null` + * to create backlog items unless the task is explicitly marked as current/active. + * * Priority can be specified in three ways: * 1. Explicit priority: `priority: { id: "..." }` - Use specific priority (disables pickPriority) * 2. Pick priority config: `pickPriority: { ... }` - Auto-select based on similarity @@ -454,26 +488,41 @@ export type PickPriorityConfig = { * * @example * ```typescript - * // Explicit priority (disables automatic matching) - * const newTask: NewActivity = { + * // "Do Now" - Action defaults to current time (actionable today) + * const urgentTask: NewActivity = { + * type: ActivityType.Action, + * title: "Review pull request" + * // Omitting start defaults to new Date() + * }; + * + * // "Do Someday" - Backlog item (recommended for most synced tasks) + * const backlogTask: NewActivity = { + * type: ActivityType.Action, + * title: "Refactor user service", + * start: null // Explicitly set to null for backlog + * }; + * + * // "Do Later" - Scheduled for specific date + * const futureTask: NewActivity = { * type: ActivityType.Action, - * title: "Review pull request", - * priority: { id: "work-project-123" } + * title: "Prepare Q1 review", + * start: new Date("2025-03-15") * }; * - * // Automatic priority matching (default behavior) - * const newNote: NewActivity = { + * // Note (typically unscheduled) + * const note: NewActivity = { * type: ActivityType.Note, * title: "Meeting notes", - * content: "Discussed Q4 roadmap..." - * // Defaults to pickPriority: { content: true } + * content: "Discussed Q4 roadmap...", + * start: null // Notes typically don't have scheduled times * }; * - * // Custom priority matching - * const newEvent: NewActivity = { + * // Event (always has explicit start/end times) + * const event: NewActivity = { * type: ActivityType.Event, * title: "Team standup", - * pickPriority: { type: true, content: 50 } + * start: new Date("2025-01-15T10:00:00"), + * end: new Date("2025-01-15T10:30:00") * }; * ``` */ diff --git a/twister/src/tag.ts b/twister/src/tag.ts index b74a0c4..48fb0f4 100644 --- a/twister/src/tag.ts +++ b/twister/src/tag.ts @@ -10,6 +10,7 @@ export enum Tag { Later = 2, Done = 3, Archived = 4, + Someday = 7, // Toggle tags Pinned = 100, diff --git a/twists/project-sync/package.json b/twists/project-sync/package.json index e2d71d3..087b9c3 100644 --- a/twists/project-sync/package.json +++ b/twists/project-sync/package.json @@ -2,7 +2,7 @@ "name": "@plotday/twist-project-sync", "plotTwistId": "c3b70e90-2e32-4293-adc0-70b66b0b602b", "displayName": "Project Sync", - "description": "Sync project management tools like Linear", + "description": "Sync project management tools like Linear, Jira, and Asana", "main": "src/index.ts", "types": "src/index.ts", "version": "0.1.0", @@ -14,7 +14,9 @@ }, "dependencies": { "@plotday/twister": "workspace:^", - "@plotday/tool-linear": "workspace:^" + "@plotday/tool-linear": "workspace:^", + "@plotday/tool-jira": "workspace:^", + "@plotday/tool-asana": "workspace:^" }, "devDependencies": { "typescript": "^5.9.3" diff --git a/twists/project-sync/src/index.ts b/twists/project-sync/src/index.ts index 93de870..3affc8e 100644 --- a/twists/project-sync/src/index.ts +++ b/twists/project-sync/src/index.ts @@ -1,3 +1,5 @@ +import { Asana } from "@plotday/tool-asana"; +import { Jira } from "@plotday/tool-jira"; import { Linear } from "@plotday/tool-linear"; import { type Activity, @@ -8,40 +10,113 @@ import { type ToolBuilder, Twist, } from "@plotday/twister"; -import type { ProjectAuth } from "@plotday/twister/common/projects"; +import type { ProjectAuth, ProjectTool } from "@plotday/twister/common/projects"; import { ActivityAccess, Plot } from "@plotday/twister/tools/plot"; +type ProjectProvider = "linear" | "jira" | "asana"; + +type StoredProjectAuth = { + provider: ProjectProvider; + authToken: string; +}; + /** * Project Sync Twist * - * Syncs project management tools (currently Linear) with Plot. + * Syncs project management tools (Linear, Jira, Asana) with Plot. * Converts issues and tasks into Plot activities with notes for comments. */ export default class ProjectSync extends Twist { build(build: ToolBuilder) { return { linear: build(Linear), + jira: build(Jira), + asana: build(Asana), plot: build(Plot, { activity: { access: ActivityAccess.Create } }), }; } + /** + * Get the tool for a specific project provider + */ + private getProviderTool(provider: ProjectProvider): ProjectTool { + switch (provider) { + case "linear": + return this.tools.linear; + case "jira": + return this.tools.jira; + case "asana": + return this.tools.asana; + default: + throw new Error(`Unknown provider: ${provider}`); + } + } + + /** + * Get stored auth for a provider + */ + private async getAuthToken(provider: ProjectProvider): Promise { + const auths = await this.getStoredAuths(); + const auth = auths.find((a) => a.provider === provider); + return auth?.authToken || null; + } + + /** + * Store auth for a provider + */ + private async addStoredAuth( + provider: ProjectProvider, + authToken: string + ): Promise { + const auths = await this.getStoredAuths(); + const existingIndex = auths.findIndex((a) => a.provider === provider); + + if (existingIndex >= 0) { + auths[existingIndex].authToken = authToken; + } else { + auths.push({ provider, authToken }); + } + + await this.set("project_auths", auths); + } + + /** + * Get all stored auths + */ + private async getStoredAuths(): Promise { + return (await this.get("project_auths")) || []; + } + /** * Called when twist is activated - * Initiates the auth flow for Linear + * Presents auth options for all supported providers */ async activate(priority: Pick) { - // Request Linear auth - const authLink = await this.tools.linear.requestAuth(this.onAuthComplete); + // Get auth links from all providers + const linearAuthLink = await this.tools.linear.requestAuth( + this.onAuthComplete, + "linear" + ); + const jiraAuthLink = await this.tools.jira.requestAuth( + this.onAuthComplete, + "jira" + ); + const asanaAuthLink = await this.tools.asana.requestAuth( + this.onAuthComplete, + "asana" + ); - // Create onboarding activity + // Create onboarding activity with all provider options + // Using start: null to create a "Do Someday" item - setup task, not urgent const activity = await this.tools.plot.createActivity({ type: ActivityType.Action, - title: "Connect Linear", + title: "Connect a project management tool", + start: null, // "Do Someday" - optional setup, not time-sensitive notes: [ { content: - "Connect your Linear account to start syncing projects and issues to Plot.", - links: [authLink], + "Connect your project management account to start syncing projects and issues to Plot. Choose one:", + links: [linearAuthLink, jiraAuthLink, asanaAuthLink], }, ], }); @@ -51,18 +126,23 @@ export default class ProjectSync extends Twist { } /** - * Called when Linear OAuth completes + * Called when OAuth completes for any provider * Fetches available projects and presents selection UI */ - async onAuthComplete(auth: ProjectAuth) { - // Store auth token - await this.set("linear_auth", auth.authToken); + async onAuthComplete(auth: ProjectAuth, provider: ProjectProvider) { + // Store auth token for this provider + await this.addStoredAuth(provider, auth.authToken); + + // Get the tool for this provider + const tool = this.getProviderTool(provider); // Fetch projects - const projects = await this.tools.linear.getProjects(auth.authToken); + const projects = await tool.getProjects(auth.authToken); if (projects.length === 0) { - await this.updateOnboardingActivity("No Linear teams found."); + await this.updateOnboardingActivity( + `No ${provider} projects found.` + ); return; } @@ -74,9 +154,12 @@ export default class ProjectSync extends Twist { }> = await Promise.all( projects.map(async (project) => ({ type: ActivityLinkType.callback as const, - title: `${project.key}: ${project.name}`, + title: project.key + ? `${project.key}: ${project.name}` + : project.name, callback: await this.callback( this.onProjectSelected, + provider, project.id, project.name ), @@ -88,7 +171,7 @@ export default class ProjectSync extends Twist { if (activity) { await this.tools.plot.createNote({ activity, - content: "Choose which Linear teams you'd like to sync to Plot:", + content: `Choose which ${provider} projects you'd like to sync to Plot:`, links, }); } @@ -98,16 +181,22 @@ export default class ProjectSync extends Twist { * Called when user selects a project to sync * Initiates the sync process for that project */ - async onProjectSelected(args: any, projectId: string, projectName: string) { - const authToken = await this.get("linear_auth"); + async onProjectSelected( + args: any, + provider: ProjectProvider, + projectId: string, + projectName: string + ) { + const authToken = await this.getAuthToken(provider); if (!authToken) { - throw new Error("No Linear auth token found"); + throw new Error(`No ${provider} auth token found`); } - // Track synced projects + // Track synced projects with provider + const syncKey = `${provider}:${projectId}`; const synced = (await this.get("synced_projects")) || []; - if (!synced.includes(projectId)) { - synced.push(projectId); + if (!synced.includes(syncKey)) { + synced.push(syncKey); await this.set("synced_projects", synced); } @@ -120,21 +209,29 @@ export default class ProjectSync extends Twist { }); } - // Start sync (last 30 days) - await this.tools.linear.startSync( + // Get the tool for this provider + const tool = this.getProviderTool(provider); + + // Start sync (full history as requested) + await tool.startSync( authToken, projectId, this.onIssue, - { timeMin: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) }, + undefined, // No time filter - sync all issues + provider, projectId ); } /** - * Called for each issue synced from Linear + * Called for each issue synced from any provider * Creates or updates Plot activities based on issue state */ - async onIssue(issue: NewActivityWithNotes, projectId: string) { + async onIssue( + issue: NewActivityWithNotes, + provider: ProjectProvider, + projectId: string + ) { // Check if issue already exists (using source for deduplication) if (issue.source) { const existing = await this.tools.plot.getActivityBySource(issue.source); @@ -151,6 +248,13 @@ export default class ProjectSync extends Twist { } } + // Default synced issues to "Do Someday" (start: null) unless already scheduled + // This prevents flooding the "Do Now" list with all synced backlog items + // The issue.start will only be set if the tool explicitly scheduled it + if (issue.type === ActivityType.Action && !("start" in issue)) { + issue.start = null; // "Do Someday" - backlog item by default + } + // Create new activity for new issue (new thread with initial note) await this.tools.plot.createActivity(issue); } @@ -161,17 +265,32 @@ export default class ProjectSync extends Twist { */ async deactivate() { // Stop all syncs - const authToken = await this.get("linear_auth"); + const auths = await this.getStoredAuths(); const synced = (await this.get("synced_projects")) || []; - if (authToken) { - for (const projectId of synced) { - await this.tools.linear.stopSync(authToken, projectId); + for (const syncKey of synced) { + // Parse provider:projectId format + const [provider, projectId] = syncKey.split(":") as [ + ProjectProvider, + string + ]; + + const authToken = await this.getAuthToken(provider); + if (authToken) { + const tool = this.getProviderTool(provider); + try { + await tool.stopSync(authToken, projectId); + } catch (error) { + console.warn( + `Failed to stop sync for ${provider}:${projectId}:`, + error + ); + } } } // Cleanup - await this.clear("linear_auth"); + await this.clear("project_auths"); await this.clear("synced_projects"); await this.clear("onboarding_activity_id"); } From c9eb53ef4f79e03b27c9d839edd15e8c7eb90669 Mon Sep 17 00:00:00 2001 From: Kris Braun Date: Sat, 3 Jan 2026 17:53:48 -0500 Subject: [PATCH 3/3] Mark initial project sync as read --- tools/asana/src/asana.ts | 29 ++++++++++++++++++++++------- tools/jira/src/jira.ts | 29 ++++++++++++++++++++++------- tools/linear/src/linear.ts | 29 ++++++++++++++++++++++------- twister/src/common/projects.ts | 16 +++++++++++++--- twists/project-sync/src/index.ts | 6 +++++- 5 files changed, 84 insertions(+), 25 deletions(-) diff --git a/tools/asana/src/asana.ts b/tools/asana/src/asana.ts index 22c1b31..a16f887 100644 --- a/tools/asana/src/asana.ts +++ b/tools/asana/src/asana.ts @@ -27,6 +27,7 @@ type SyncState = { offset: number; batchNumber: number; tasksProcessed: number; + initialSync: boolean; }; /** @@ -148,13 +149,21 @@ export class Asana extends Tool implements ProjectTool { * Start syncing tasks from an Asana project */ async startSync< - TCallback extends (task: NewActivityWithNotes, ...args: any[]) => any + TCallback extends ( + task: NewActivityWithNotes, + syncMeta: { initialSync: boolean }, + ...args: any[] + ) => any >( authToken: string, projectId: string, callback: TCallback, options?: ProjectSyncOptions, - ...extraArgs: TCallback extends (task: any, ...rest: infer R) => any + ...extraArgs: TCallback extends ( + task: any, + syncMeta: any, + ...rest: infer R + ) => any ? R : [] ): Promise { @@ -198,6 +207,7 @@ export class Asana extends Tool implements ProjectTool { offset: 0, batchNumber: 1, tasksProcessed: 0, + initialSync: true, }); // Queue first batch @@ -268,8 +278,12 @@ export class Asana extends Tool implements ProjectTool { task, projectId ); - // Execute the callback using the callback token - await this.tools.callbacks.run(callbackToken, activityWithNotes); + // Execute the callback using the callback token with syncMeta + await this.tools.callbacks.run( + callbackToken, + activityWithNotes, + { initialSync: state.initialSync } + ); } // Check if more pages by checking if we got a full batch @@ -280,6 +294,7 @@ export class Asana extends Tool implements ProjectTool { offset: state.offset + batchSize, batchNumber: state.batchNumber + 1, tasksProcessed: state.tasksProcessed + tasksResult.data.length, + initialSync: state.initialSync, }); // Queue next batch @@ -291,7 +306,7 @@ export class Asana extends Tool implements ProjectTool { ); await this.tools.tasks.runTask(nextBatch); } else { - // Cleanup sync state + // Initial sync is complete - cleanup sync state await this.clear(`sync_state_${projectId}`); } } @@ -423,8 +438,8 @@ export class Asana extends Tool implements ProjectTool { projectId ); - // Execute stored callback - await this.run(callbackToken, activityWithNotes); + // Execute stored callback - webhooks are never part of initial sync + await this.tools.callbacks.run(callbackToken, activityWithNotes, { initialSync: false }); } catch (error) { console.warn("Failed to process Asana task webhook:", error); } diff --git a/tools/jira/src/jira.ts b/tools/jira/src/jira.ts index a1838b5..68e2850 100644 --- a/tools/jira/src/jira.ts +++ b/tools/jira/src/jira.ts @@ -27,6 +27,7 @@ type SyncState = { startAt: number; batchNumber: number; issuesProcessed: number; + initialSync: boolean; }; /** @@ -154,13 +155,21 @@ export class Jira extends Tool implements ProjectTool { * Start syncing issues from a Jira project */ async startSync< - TCallback extends (issue: NewActivityWithNotes, ...args: any[]) => any + TCallback extends ( + issue: NewActivityWithNotes, + syncMeta: { initialSync: boolean }, + ...args: any[] + ) => any >( authToken: string, projectId: string, callback: TCallback, options?: ProjectSyncOptions, - ...extraArgs: TCallback extends (issue: any, ...rest: infer R) => any + ...extraArgs: TCallback extends ( + issue: any, + syncMeta: any, + ...rest: infer R + ) => any ? R : [] ): Promise { @@ -204,6 +213,7 @@ export class Jira extends Tool implements ProjectTool { startAt: 0, batchNumber: 1, issuesProcessed: 0, + initialSync: true, }); // Queue first batch @@ -269,8 +279,12 @@ export class Jira extends Tool implements ProjectTool { issue, projectId ); - // Execute the callback using the callback token - await this.tools.callbacks.run(callbackToken, activityWithNotes); + // Execute the callback using the callback token with syncMeta + await this.tools.callbacks.run( + callbackToken, + activityWithNotes, + { initialSync: state.initialSync } + ); } // Check if more pages @@ -282,6 +296,7 @@ export class Jira extends Tool implements ProjectTool { startAt: nextStartAt, batchNumber: state.batchNumber + 1, issuesProcessed: state.issuesProcessed + (searchResult.issues?.length || 0), + initialSync: state.initialSync, }); // Queue next batch @@ -293,7 +308,7 @@ export class Jira extends Tool implements ProjectTool { ); await this.tools.tasks.runTask(nextBatch); } else { - // Cleanup sync state + // Initial sync is complete - cleanup sync state await this.clear(`sync_state_${projectId}`); } } @@ -402,8 +417,8 @@ export class Jira extends Tool implements ProjectTool { projectId ); - // Execute stored callback - await this.run(callbackToken, activityWithNotes); + // Execute stored callback - webhooks are never part of initial sync + await this.tools.callbacks.run(callbackToken, activityWithNotes, { initialSync: false }); } } diff --git a/tools/linear/src/linear.ts b/tools/linear/src/linear.ts index ace9e34..d8b3e4c 100644 --- a/tools/linear/src/linear.ts +++ b/tools/linear/src/linear.ts @@ -27,6 +27,7 @@ type SyncState = { after: string | null; batchNumber: number; issuesProcessed: number; + initialSync: boolean; }; /** @@ -133,13 +134,21 @@ export class Linear extends Tool implements ProjectTool { * Start syncing issues from a Linear team */ async startSync< - TCallback extends (issue: NewActivityWithNotes, ...args: any[]) => any + TCallback extends ( + issue: NewActivityWithNotes, + syncMeta: { initialSync: boolean }, + ...args: any[] + ) => any >( authToken: string, projectId: string, callback: TCallback, options?: ProjectSyncOptions, - ...extraArgs: TCallback extends (issue: any, ...rest: infer R) => any + ...extraArgs: TCallback extends ( + issue: any, + syncMeta: any, + ...rest: infer R + ) => any ? R : [] ): Promise { @@ -218,6 +227,7 @@ export class Linear extends Tool implements ProjectTool { after: null, batchNumber: 1, issuesProcessed: 0, + initialSync: true, }); // Queue first batch @@ -272,8 +282,12 @@ export class Linear extends Tool implements ProjectTool { issue, projectId ); - // Execute the callback using the callback token - await this.tools.callbacks.run(callbackToken, activityWithNotes); + // Execute the callback using the callback token with syncMeta + await this.tools.callbacks.run( + callbackToken, + activityWithNotes, + { initialSync: state.initialSync } + ); } // Check if more pages @@ -282,6 +296,7 @@ export class Linear extends Tool implements ProjectTool { after: issuesConnection.pageInfo.endCursor, batchNumber: state.batchNumber + 1, issuesProcessed: state.issuesProcessed + issuesConnection.nodes.length, + initialSync: state.initialSync, }); // Queue next batch @@ -293,7 +308,7 @@ export class Linear extends Tool implements ProjectTool { ); await this.tools.tasks.runTask(nextBatch); } else { - // Cleanup sync state + // Initial sync is complete - cleanup sync state await this.clear(`sync_state_${projectId}`); } } @@ -430,8 +445,8 @@ export class Linear extends Tool implements ProjectTool { projectId ); - // Execute stored callback - await this.run(callbackToken, activityWithNotes); + // Execute stored callback - webhooks are never part of initial sync + await this.tools.callbacks.run(callbackToken, activityWithNotes, { initialSync: false }); } } diff --git a/twister/src/common/projects.ts b/twister/src/common/projects.ts index 2fdef15..86a463f 100644 --- a/twister/src/common/projects.ts +++ b/twister/src/common/projects.ts @@ -82,19 +82,29 @@ export interface ProjectTool { * * @param authToken - Authorization token for access * @param projectId - ID of the project to sync - * @param callback - Function receiving (issue, ...extraArgs) for each synced issue + * @param callback - Function receiving (issue, syncMeta, ...extraArgs) for each synced issue. + * The syncMeta parameter contains { initialSync: boolean } to indicate if this is + * part of the initial sync or an incremental update. * @param options - Optional configuration for limiting the sync scope (e.g., time range) * @param extraArgs - Additional arguments to pass to the callback (type-checked) * @returns Promise that resolves when sync setup is complete */ startSync< - TCallback extends (issue: NewActivityWithNotes, ...args: any[]) => any + TCallback extends ( + issue: NewActivityWithNotes, + syncMeta: { initialSync: boolean }, + ...args: any[] + ) => any >( authToken: string, projectId: string, callback: TCallback, options?: ProjectSyncOptions, - ...extraArgs: TCallback extends (issue: any, ...rest: infer R) => any + ...extraArgs: TCallback extends ( + issue: any, + syncMeta: any, + ...rest: infer R + ) => any ? R : [] ): Promise; diff --git a/twists/project-sync/src/index.ts b/twists/project-sync/src/index.ts index 3affc8e..4a80a40 100644 --- a/twists/project-sync/src/index.ts +++ b/twists/project-sync/src/index.ts @@ -229,6 +229,7 @@ export default class ProjectSync extends Twist { */ async onIssue( issue: NewActivityWithNotes, + syncMeta: { initialSync: boolean }, provider: ProjectProvider, projectId: string ) { @@ -256,7 +257,10 @@ export default class ProjectSync extends Twist { } // Create new activity for new issue (new thread with initial note) - await this.tools.plot.createActivity(issue); + // Mark existing issues as read during initial sync to avoid overwhelming the user + await this.tools.plot.createActivity(issue, { + unread: !syncMeta.initialSync, + }); } /**