runline 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/.pi/extensions/runline-context/index.ts +135 -0
  2. package/.pi/extensions/runline-context/package.json +17 -0
  3. package/README.md +273 -0
  4. package/dist/commands/actions.d.ts +3 -0
  5. package/dist/commands/actions.js +43 -0
  6. package/dist/commands/connection.d.ts +11 -0
  7. package/dist/commands/connection.js +56 -0
  8. package/dist/commands/exec.d.ts +5 -0
  9. package/dist/commands/exec.js +46 -0
  10. package/dist/commands/init.d.ts +4 -0
  11. package/dist/commands/init.js +26 -0
  12. package/dist/commands/plugin.d.ts +10 -0
  13. package/dist/commands/plugin.js +57 -0
  14. package/dist/config/index.d.ts +3 -0
  15. package/dist/config/index.js +2 -0
  16. package/dist/config/loader.d.ts +11 -0
  17. package/dist/config/loader.js +82 -0
  18. package/dist/config/types.d.ts +9 -0
  19. package/dist/config/types.js +5 -0
  20. package/dist/core/engine.d.ts +21 -0
  21. package/dist/core/engine.js +280 -0
  22. package/dist/index.d.ts +16 -0
  23. package/dist/index.js +9 -0
  24. package/dist/main.d.ts +2 -0
  25. package/dist/main.js +127 -0
  26. package/dist/plugin/api.d.ts +32 -0
  27. package/dist/plugin/api.js +68 -0
  28. package/dist/plugin/installer.d.ts +27 -0
  29. package/dist/plugin/installer.js +181 -0
  30. package/dist/plugin/loader.d.ts +13 -0
  31. package/dist/plugin/loader.js +164 -0
  32. package/dist/plugin/registry.d.ts +18 -0
  33. package/dist/plugin/registry.js +43 -0
  34. package/dist/plugin/types.d.ts +40 -0
  35. package/dist/plugin/types.js +1 -0
  36. package/dist/plugins/actionNetwork/src/index.js +353 -0
  37. package/dist/plugins/activeCampaign/src/index.js +711 -0
  38. package/dist/plugins/adalo/src/index.js +131 -0
  39. package/dist/plugins/affinity/src/index.js +279 -0
  40. package/dist/plugins/agileCrm/src/index.js +415 -0
  41. package/dist/plugins/airtable/src/index.js +280 -0
  42. package/dist/plugins/airtop/src/index.js +527 -0
  43. package/dist/plugins/apiTemplateIo/src/index.js +86 -0
  44. package/dist/plugins/asana/src/index.js +413 -0
  45. package/dist/plugins/autopilot/src/index.js +203 -0
  46. package/dist/plugins/bambooHr/src/index.js +252 -0
  47. package/dist/plugins/bannerbear/src/index.js +100 -0
  48. package/dist/plugins/baserow/src/index.js +180 -0
  49. package/dist/plugins/beeminder/src/index.js +298 -0
  50. package/dist/plugins/bitly/src/index.js +107 -0
  51. package/dist/plugins/bitwarden/src/index.js +383 -0
  52. package/dist/plugins/box/src/index.js +300 -0
  53. package/dist/plugins/brandfetch/src/index.js +80 -0
  54. package/dist/plugins/brevo/src/index.js +305 -0
  55. package/dist/plugins/bubble/src/index.js +181 -0
  56. package/dist/plugins/chargebee/src/index.js +126 -0
  57. package/dist/plugins/circleci/src/index.js +111 -0
  58. package/dist/plugins/ciscoWebex/src/index.js +245 -0
  59. package/dist/plugins/clearbit/src/index.js +103 -0
  60. package/dist/plugins/clickup/src/index.js +1043 -0
  61. package/dist/plugins/clockify/src/index.js +443 -0
  62. package/dist/plugins/cloudflare/src/index.js +93 -0
  63. package/dist/plugins/cockpit/src/index.js +131 -0
  64. package/dist/plugins/coda/src/index.js +327 -0
  65. package/dist/plugins/coingecko/src/index.js +244 -0
  66. package/dist/plugins/contentful/src/index.js +146 -0
  67. package/dist/plugins/convertkit/src/index.js +270 -0
  68. package/dist/plugins/copper/src/index.js +140 -0
  69. package/dist/plugins/cortex/src/index.js +147 -0
  70. package/dist/plugins/currents/src/index.js +405 -0
  71. package/dist/plugins/customerIo/src/index.js +184 -0
  72. package/dist/plugins/databricks/src/index.js +342 -0
  73. package/dist/plugins/deepl/src/index.js +87 -0
  74. package/dist/plugins/demio/src/index.js +111 -0
  75. package/dist/plugins/dhl/src/index.js +40 -0
  76. package/dist/plugins/discord/src/index.js +275 -0
  77. package/dist/plugins/discourse/src/index.js +273 -0
  78. package/dist/plugins/disqus/src/index.js +145 -0
  79. package/dist/plugins/docker/src/index.js +76 -0
  80. package/dist/plugins/drift/src/index.js +89 -0
  81. package/dist/plugins/dropbox/src/index.js +159 -0
  82. package/dist/plugins/dropcontact/src/index.js +59 -0
  83. package/dist/plugins/egoi/src/index.js +151 -0
  84. package/dist/plugins/elasticsearch/src/index.js +157 -0
  85. package/dist/plugins/emelia/src/index.js +174 -0
  86. package/dist/plugins/erpnext/src/index.js +121 -0
  87. package/dist/plugins/facebookGraph/src/index.js +57 -0
  88. package/dist/plugins/freshdesk/src/index.js +320 -0
  89. package/dist/plugins/freshservice/src/index.js +146 -0
  90. package/dist/plugins/freshworksCrm/src/index.js +149 -0
  91. package/dist/plugins/getresponse/src/index.js +140 -0
  92. package/dist/plugins/ghost/src/index.js +192 -0
  93. package/dist/plugins/github/src/index.js +630 -0
  94. package/dist/plugins/gitlab/src/index.js +358 -0
  95. package/dist/plugins/gong/src/index.js +126 -0
  96. package/dist/plugins/gotify/src/index.js +77 -0
  97. package/dist/plugins/gotowebinar/src/index.js +316 -0
  98. package/dist/plugins/grafana/src/index.js +250 -0
  99. package/dist/plugins/graphql/src/index.js +78 -0
  100. package/dist/plugins/grist/src/index.js +106 -0
  101. package/dist/plugins/hackernews/src/index.js +89 -0
  102. package/dist/plugins/halopsa/src/index.js +79 -0
  103. package/dist/plugins/harvest/src/index.js +163 -0
  104. package/dist/plugins/helpscout/src/index.js +176 -0
  105. package/dist/plugins/highlevel/src/index.js +172 -0
  106. package/dist/plugins/homeAssistant/src/index.js +148 -0
  107. package/dist/plugins/hubspot/src/index.js +176 -0
  108. package/dist/plugins/humanticAi/src/index.js +60 -0
  109. package/dist/plugins/hunter/src/index.js +59 -0
  110. package/dist/plugins/intercom/src/index.js +156 -0
  111. package/dist/plugins/iterable/src/index.js +139 -0
  112. package/dist/plugins/jenkins/src/index.js +132 -0
  113. package/dist/plugins/jira/src/index.js +229 -0
  114. package/dist/plugins/keap/src/index.js +502 -0
  115. package/dist/plugins/kobotoolbox/src/index.js +281 -0
  116. package/dist/plugins/lemlist/src/index.js +231 -0
  117. package/dist/plugins/linear/src/index.js +133 -0
  118. package/dist/plugins/lingvanex/src/index.js +31 -0
  119. package/dist/plugins/linkedin/src/index.js +80 -0
  120. package/dist/plugins/lonescale/src/index.js +119 -0
  121. package/dist/plugins/magento/src/index.js +300 -0
  122. package/dist/plugins/mailcheck/src/index.js +27 -0
  123. package/dist/plugins/mailchimp/src/index.js +321 -0
  124. package/dist/plugins/mailerlite/src/index.js +123 -0
  125. package/dist/plugins/mailgun/src/index.js +48 -0
  126. package/dist/plugins/mailjet/src/index.js +155 -0
  127. package/dist/plugins/mandrill/src/index.js +145 -0
  128. package/dist/plugins/marketstack/src/index.js +97 -0
  129. package/dist/plugins/matrix/src/index.js +194 -0
  130. package/dist/plugins/mattermost/src/index.js +331 -0
  131. package/dist/plugins/mautic/src/index.js +311 -0
  132. package/dist/plugins/medium/src/index.js +77 -0
  133. package/dist/plugins/messagebird/src/index.js +57 -0
  134. package/dist/plugins/metabase/src/index.js +130 -0
  135. package/dist/plugins/misp/src/index.js +476 -0
  136. package/dist/plugins/mocean/src/index.js +67 -0
  137. package/dist/plugins/monday/src/index.js +231 -0
  138. package/dist/plugins/monicaCrm/src/index.js +52 -0
  139. package/dist/plugins/msg91/src/index.js +31 -0
  140. package/dist/plugins/nasa/src/index.js +146 -0
  141. package/dist/plugins/netlify/src/index.js +151 -0
  142. package/dist/plugins/netscalerAdc/src/index.js +131 -0
  143. package/dist/plugins/nextcloud/src/index.js +263 -0
  144. package/dist/plugins/nocodb/src/index.js +130 -0
  145. package/dist/plugins/notion/src/index.js +112 -0
  146. package/dist/plugins/npm/src/index.js +104 -0
  147. package/dist/plugins/odoo/src/index.js +157 -0
  148. package/dist/plugins/okta/src/index.js +141 -0
  149. package/dist/plugins/oneSimpleApi/src/index.js +155 -0
  150. package/dist/plugins/onfleet/src/index.js +254 -0
  151. package/dist/plugins/openThesaurus/src/index.js +32 -0
  152. package/dist/plugins/openweathermap/src/index.js +60 -0
  153. package/dist/plugins/oura/src/index.js +62 -0
  154. package/dist/plugins/paddle/src/index.js +247 -0
  155. package/dist/plugins/pagerduty/src/index.js +201 -0
  156. package/dist/plugins/paypal/src/index.js +106 -0
  157. package/dist/plugins/peekalink/src/index.js +35 -0
  158. package/dist/plugins/phantombuster/src/index.js +94 -0
  159. package/dist/plugins/philipsHue/src/index.js +98 -0
  160. package/dist/plugins/pipedrive/src/index.js +169 -0
  161. package/dist/plugins/plivo/src/index.js +66 -0
  162. package/dist/plugins/postbin/src/index.js +93 -0
  163. package/dist/plugins/posthog/src/index.js +113 -0
  164. package/dist/plugins/profitwell/src/index.js +50 -0
  165. package/dist/plugins/pushbullet/src/index.js +102 -0
  166. package/dist/plugins/pushcut/src/index.js +39 -0
  167. package/dist/plugins/pushover/src/index.js +65 -0
  168. package/dist/plugins/quickbase/src/index.js +153 -0
  169. package/dist/plugins/quickbooks/src/index.js +73 -0
  170. package/dist/plugins/quickchart/src/index.js +36 -0
  171. package/dist/plugins/raindrop/src/index.js +209 -0
  172. package/dist/plugins/reddit/src/index.js +185 -0
  173. package/dist/plugins/rocketchat/src/index.js +53 -0
  174. package/dist/plugins/rundeck/src/index.js +62 -0
  175. package/dist/plugins/salesforce/src/index.js +94 -0
  176. package/dist/plugins/salesmate/src/index.js +83 -0
  177. package/dist/plugins/securityScorecard/src/index.js +200 -0
  178. package/dist/plugins/segment/src/index.js +125 -0
  179. package/dist/plugins/sendgrid/src/index.js +187 -0
  180. package/dist/plugins/sendy/src/index.js +138 -0
  181. package/dist/plugins/sentry/src/index.js +233 -0
  182. package/dist/plugins/servicenow/src/index.js +108 -0
  183. package/dist/plugins/shopify/src/index.js +222 -0
  184. package/dist/plugins/signl4/src/index.js +61 -0
  185. package/dist/plugins/slack/src/index.js +236 -0
  186. package/dist/plugins/sms77/src/index.js +63 -0
  187. package/dist/plugins/splunk/src/index.js +207 -0
  188. package/dist/plugins/spotify/src/index.js +188 -0
  189. package/dist/plugins/stackby/src/index.js +82 -0
  190. package/dist/plugins/storyblok/src/index.js +141 -0
  191. package/dist/plugins/strapi/src/index.js +152 -0
  192. package/dist/plugins/strava/src/index.js +137 -0
  193. package/dist/plugins/stripe/src/index.js +222 -0
  194. package/dist/plugins/supabase/src/index.js +121 -0
  195. package/dist/plugins/syncromsp/src/index.js +255 -0
  196. package/dist/plugins/tapfiliate/src/index.js +125 -0
  197. package/dist/plugins/telegram/src/index.js +233 -0
  198. package/dist/plugins/thehive/src/index.js +142 -0
  199. package/dist/plugins/thehiveProject/src/index.js +194 -0
  200. package/dist/plugins/todoist/src/index.js +244 -0
  201. package/dist/plugins/travisci/src/index.js +71 -0
  202. package/dist/plugins/trello/src/index.js +341 -0
  203. package/dist/plugins/twake/src/index.js +40 -0
  204. package/dist/plugins/twilio/src/index.js +75 -0
  205. package/dist/plugins/twist/src/index.js +90 -0
  206. package/dist/plugins/twitter/src/index.js +123 -0
  207. package/dist/plugins/unleashedSoftware/src/index.js +84 -0
  208. package/dist/plugins/uplead/src/index.js +59 -0
  209. package/dist/plugins/uproc/src/index.js +34 -0
  210. package/dist/plugins/uptimerobot/src/index.js +264 -0
  211. package/dist/plugins/urlscanio/src/index.js +64 -0
  212. package/dist/plugins/vero/src/index.js +80 -0
  213. package/dist/plugins/vonage/src/index.js +42 -0
  214. package/dist/plugins/wekan/src/index.js +91 -0
  215. package/dist/plugins/woocommerce/src/index.js +92 -0
  216. package/dist/plugins/wordpress/src/index.js +121 -0
  217. package/dist/plugins/xero/src/index.js +136 -0
  218. package/dist/plugins/yourls/src/index.js +56 -0
  219. package/dist/plugins/zammad/src/index.js +91 -0
  220. package/dist/plugins/zendesk/src/index.js +137 -0
  221. package/dist/plugins/zoho/src/index.js +85 -0
  222. package/dist/plugins/zoom/src/index.js +122 -0
  223. package/dist/plugins/zulip/src/index.js +170 -0
  224. package/dist/sdk.d.ts +38 -0
  225. package/dist/sdk.js +105 -0
  226. package/dist/utils/cli.d.ts +13 -0
  227. package/dist/utils/cli.js +32 -0
  228. package/dist/utils/output.d.ts +4 -0
  229. package/dist/utils/output.js +13 -0
  230. package/package.json +57 -0
@@ -0,0 +1,106 @@
1
+ async function apiRequest(baseUrl, apiKey, method, endpoint, body, qs) {
2
+ const url = new URL(`${baseUrl}/api${endpoint}`);
3
+ if (qs) {
4
+ for (const [k, v] of Object.entries(qs)) {
5
+ if (v !== undefined && v !== null)
6
+ url.searchParams.set(k, String(v));
7
+ }
8
+ }
9
+ const opts = {
10
+ method,
11
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
12
+ };
13
+ if (body !== undefined && method !== "GET" && method !== "DELETE") {
14
+ opts.body = JSON.stringify(body);
15
+ }
16
+ const res = await fetch(url.toString(), opts);
17
+ if (!res.ok)
18
+ throw new Error(`Grist API error ${res.status}: ${await res.text()}`);
19
+ if (res.status === 204)
20
+ return { success: true };
21
+ return res.json();
22
+ }
23
+ function getConn(ctx) {
24
+ const cfg = ctx.connection.config;
25
+ const planType = cfg.planType ?? "free";
26
+ let baseUrl;
27
+ if (planType === "selfHosted")
28
+ baseUrl = cfg.selfHostedUrl.replace(/\/$/, "");
29
+ else if (planType === "paid")
30
+ baseUrl = `https://${cfg.subdomain}.getgrist.com`;
31
+ else
32
+ baseUrl = "https://docs.getgrist.com";
33
+ return { baseUrl, apiKey: cfg.apiKey };
34
+ }
35
+ export default function grist(rl) {
36
+ rl.setName("grist");
37
+ rl.setVersion("0.1.0");
38
+ rl.setConnectionSchema({
39
+ apiKey: { type: "string", required: true, description: "Grist API key", env: "GRIST_API_KEY" },
40
+ planType: { type: "string", required: false, description: "free (default), paid, or selfHosted", default: "free" },
41
+ subdomain: { type: "string", required: false, description: "Subdomain for paid plan" },
42
+ selfHostedUrl: { type: "string", required: false, description: "Full URL for self-hosted", env: "GRIST_URL" },
43
+ });
44
+ rl.registerAction("record.create", {
45
+ description: "Create records in a table",
46
+ inputSchema: {
47
+ docId: { type: "string", required: true, description: "Document ID" },
48
+ tableId: { type: "string", required: true, description: "Table ID" },
49
+ records: { type: "array", required: true, description: "Array of {fields: {col: value}} objects" },
50
+ },
51
+ async execute(input, ctx) {
52
+ const { docId, tableId, records } = input;
53
+ const { baseUrl, apiKey } = getConn(ctx);
54
+ return apiRequest(baseUrl, apiKey, "POST", `/docs/${docId}/tables/${tableId}/records`, { records });
55
+ },
56
+ });
57
+ rl.registerAction("record.list", {
58
+ description: "List records from a table",
59
+ inputSchema: {
60
+ docId: { type: "string", required: true, description: "Document ID" },
61
+ tableId: { type: "string", required: true, description: "Table ID" },
62
+ limit: { type: "number", required: false, description: "Max results" },
63
+ sort: { type: "string", required: false, description: "Sort columns (e.g. 'Name,-Age')" },
64
+ filter: { type: "object", required: false, description: "Filter as {column: [values]}" },
65
+ },
66
+ async execute(input, ctx) {
67
+ const { docId, tableId, limit, sort, filter } = (input ?? {});
68
+ const { baseUrl, apiKey } = getConn(ctx);
69
+ const qs = {};
70
+ if (limit)
71
+ qs.limit = limit;
72
+ if (sort)
73
+ qs.sort = sort;
74
+ if (filter)
75
+ qs.filter = JSON.stringify(filter);
76
+ const data = (await apiRequest(baseUrl, apiKey, "GET", `/docs/${docId}/tables/${tableId}/records`, undefined, qs));
77
+ return data.records;
78
+ },
79
+ });
80
+ rl.registerAction("record.update", {
81
+ description: "Update records in a table",
82
+ inputSchema: {
83
+ docId: { type: "string", required: true, description: "Document ID" },
84
+ tableId: { type: "string", required: true, description: "Table ID" },
85
+ records: { type: "array", required: true, description: "Array of {id, fields: {col: value}} objects" },
86
+ },
87
+ async execute(input, ctx) {
88
+ const { docId, tableId, records } = input;
89
+ const { baseUrl, apiKey } = getConn(ctx);
90
+ return apiRequest(baseUrl, apiKey, "PATCH", `/docs/${docId}/tables/${tableId}/records`, { records });
91
+ },
92
+ });
93
+ rl.registerAction("record.delete", {
94
+ description: "Delete records from a table",
95
+ inputSchema: {
96
+ docId: { type: "string", required: true, description: "Document ID" },
97
+ tableId: { type: "string", required: true, description: "Table ID" },
98
+ rowIds: { type: "array", required: true, description: "Array of row IDs to delete" },
99
+ },
100
+ async execute(input, ctx) {
101
+ const { docId, tableId, rowIds } = input;
102
+ const { baseUrl, apiKey } = getConn(ctx);
103
+ return apiRequest(baseUrl, apiKey, "POST", `/docs/${docId}/tables/${tableId}/data/delete`, rowIds);
104
+ },
105
+ });
106
+ }
@@ -0,0 +1,89 @@
1
+ const ALGOLIA_URL = "http://hn.algolia.com/api/v1";
2
+ const FIREBASE_URL = "https://hacker-news.firebaseio.com/v0";
3
+ async function algoliaRequest(endpoint, qs) {
4
+ const url = new URL(`${ALGOLIA_URL}/${endpoint}`);
5
+ if (qs) {
6
+ for (const [k, v] of Object.entries(qs)) {
7
+ if (v !== undefined && v !== null)
8
+ url.searchParams.set(k, String(v));
9
+ }
10
+ }
11
+ const res = await fetch(url.toString());
12
+ if (!res.ok)
13
+ throw new Error(`HN API error ${res.status}: ${await res.text()}`);
14
+ return res.json();
15
+ }
16
+ async function firebaseRequest(endpoint) {
17
+ const res = await fetch(`${FIREBASE_URL}/${endpoint}.json`);
18
+ if (!res.ok)
19
+ throw new Error(`HN Firebase API error ${res.status}: ${await res.text()}`);
20
+ return res.json();
21
+ }
22
+ export default function hackernews(rl) {
23
+ rl.setName("hackernews");
24
+ rl.setVersion("0.1.0");
25
+ // No auth needed
26
+ rl.setConnectionSchema({});
27
+ rl.registerAction("article.get", {
28
+ description: "Get a Hacker News article/item by ID",
29
+ inputSchema: { articleId: { type: "string", required: true, description: "Item ID" } },
30
+ async execute(input) {
31
+ return firebaseRequest(`item/${input.articleId}`);
32
+ },
33
+ });
34
+ rl.registerAction("article.search", {
35
+ description: "Search Hacker News articles",
36
+ inputSchema: {
37
+ query: { type: "string", required: true, description: "Search query" },
38
+ tags: { type: "string", required: false, description: "Filter tags: story, comment, ask_hn, show_hn, poll, front_page" },
39
+ limit: { type: "number", required: false, description: "Max results" },
40
+ },
41
+ async execute(input) {
42
+ const { query, tags, limit } = (input ?? {});
43
+ const qs = { query };
44
+ if (tags)
45
+ qs.tags = tags;
46
+ if (limit)
47
+ qs.hitsPerPage = limit;
48
+ const data = (await algoliaRequest("search", qs));
49
+ return data.hits;
50
+ },
51
+ });
52
+ rl.registerAction("user.get", {
53
+ description: "Get a Hacker News user by username",
54
+ inputSchema: { username: { type: "string", required: true, description: "Username" } },
55
+ async execute(input) {
56
+ return firebaseRequest(`user/${input.username}`);
57
+ },
58
+ });
59
+ rl.registerAction("all.top", {
60
+ description: "Get top stories",
61
+ inputSchema: { limit: { type: "number", required: false, description: "Max results (default: 30)" } },
62
+ async execute(input) {
63
+ const { limit = 30 } = (input ?? {});
64
+ const ids = (await firebaseRequest("topstories"));
65
+ const items = await Promise.all(ids.slice(0, limit).map((id) => firebaseRequest(`item/${id}`)));
66
+ return items;
67
+ },
68
+ });
69
+ rl.registerAction("all.new", {
70
+ description: "Get newest stories",
71
+ inputSchema: { limit: { type: "number", required: false, description: "Max results (default: 30)" } },
72
+ async execute(input) {
73
+ const { limit = 30 } = (input ?? {});
74
+ const ids = (await firebaseRequest("newstories"));
75
+ const items = await Promise.all(ids.slice(0, limit).map((id) => firebaseRequest(`item/${id}`)));
76
+ return items;
77
+ },
78
+ });
79
+ rl.registerAction("all.best", {
80
+ description: "Get best stories",
81
+ inputSchema: { limit: { type: "number", required: false, description: "Max results (default: 30)" } },
82
+ async execute(input) {
83
+ const { limit = 30 } = (input ?? {});
84
+ const ids = (await firebaseRequest("beststories"));
85
+ const items = await Promise.all(ids.slice(0, limit).map((id) => firebaseRequest(`item/${id}`)));
86
+ return items;
87
+ },
88
+ });
89
+ }
@@ -0,0 +1,79 @@
1
+ let cachedToken = null;
2
+ async function getToken(authUrl, clientId, clientSecret, scope, tenant) {
3
+ if (cachedToken && Date.now() < cachedToken.expiresAt)
4
+ return cachedToken.token;
5
+ const url = tenant ? `${authUrl}?tenant=${tenant}` : authUrl;
6
+ const body = new URLSearchParams({ grant_type: "client_credentials", client_id: clientId, client_secret: clientSecret, scope });
7
+ const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: body.toString() });
8
+ if (!res.ok)
9
+ throw new Error(`HaloPSA auth error ${res.status}: ${await res.text()}`);
10
+ const data = (await res.json());
11
+ cachedToken = { token: data.access_token, expiresAt: Date.now() + (data.expires_in - 60) * 1000 };
12
+ return cachedToken.token;
13
+ }
14
+ async function apiRequest(apiUrl, token, method, endpoint, body, qs) {
15
+ const url = new URL(`${apiUrl}${endpoint}`);
16
+ if (qs) {
17
+ for (const [k, v] of Object.entries(qs)) {
18
+ if (v !== undefined && v !== null)
19
+ url.searchParams.set(k, String(v));
20
+ }
21
+ }
22
+ const opts = { method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" } };
23
+ if (body && Object.keys(body).length > 0 && method !== "GET" && method !== "DELETE")
24
+ opts.body = JSON.stringify(body);
25
+ const res = await fetch(url.toString(), opts);
26
+ if (!res.ok)
27
+ throw new Error(`HaloPSA API error ${res.status}: ${await res.text()}`);
28
+ if (res.status === 204)
29
+ return { success: true };
30
+ return res.json();
31
+ }
32
+ async function req(ctx, method, endpoint, body, qs) {
33
+ const cfg = ctx.connection.config;
34
+ const authUrl = cfg.hostingType === "on-premise" ? `${cfg.appUrl}/auth/token` : `${cfg.authUrl}/token`;
35
+ const token = await getToken(authUrl, cfg.clientId, cfg.clientSecret, cfg.scope ?? "all", cfg.tenant);
36
+ return apiRequest(cfg.resourceApiUrl.replace(/\/$/, ""), token, method, endpoint, body, qs);
37
+ }
38
+ function registerCrud(rl, resource, apiPath) {
39
+ rl.registerAction(`${resource}.create`, {
40
+ description: `Create a ${resource}`, inputSchema: { properties: { type: "object", required: true, description: `${resource} data` } },
41
+ async execute(input, ctx) { return req(ctx, "POST", apiPath, { ...input.properties }); },
42
+ });
43
+ rl.registerAction(`${resource}.get`, {
44
+ description: `Get a ${resource}`, inputSchema: { id: { type: "number", required: true, description: `${resource} ID` } },
45
+ async execute(input, ctx) { return req(ctx, "GET", `${apiPath}/${input.id}`); },
46
+ });
47
+ rl.registerAction(`${resource}.list`, {
48
+ description: `List ${resource}s`, inputSchema: { limit: { type: "number", required: false, description: "Max results" }, page: { type: "number", required: false, description: "Page" } },
49
+ async execute(input, ctx) { const { limit, page } = (input ?? {}); const qs = {}; if (limit)
50
+ qs.count = limit; if (page)
51
+ qs.page_no = page; return req(ctx, "GET", apiPath, undefined, qs); },
52
+ });
53
+ rl.registerAction(`${resource}.update`, {
54
+ description: `Update a ${resource}`, inputSchema: { id: { type: "number", required: true, description: `${resource} ID` }, properties: { type: "object", required: true, description: "Fields to update" } },
55
+ async execute(input, ctx) { const { id, properties } = input; return req(ctx, "PUT", apiPath, { id, ...properties }); },
56
+ });
57
+ rl.registerAction(`${resource}.delete`, {
58
+ description: `Delete a ${resource}`, inputSchema: { id: { type: "number", required: true, description: `${resource} ID` } },
59
+ async execute(input, ctx) { await req(ctx, "DELETE", `${apiPath}/${input.id}`); return { success: true }; },
60
+ });
61
+ }
62
+ export default function halopsa(rl) {
63
+ rl.setName("halopsa");
64
+ rl.setVersion("0.1.0");
65
+ rl.setConnectionSchema({
66
+ hostingType: { type: "string", required: false, description: "cloud (default) or on-premise", default: "cloud" },
67
+ authUrl: { type: "string", required: false, description: "Auth server URL (cloud)", env: "HALOPSA_AUTH_URL" },
68
+ appUrl: { type: "string", required: false, description: "App URL (on-premise)", env: "HALOPSA_APP_URL" },
69
+ resourceApiUrl: { type: "string", required: true, description: "Resource API URL", env: "HALOPSA_API_URL" },
70
+ clientId: { type: "string", required: true, description: "OAuth2 client ID", env: "HALOPSA_CLIENT_ID" },
71
+ clientSecret: { type: "string", required: true, description: "OAuth2 client secret", env: "HALOPSA_CLIENT_SECRET" },
72
+ scope: { type: "string", required: false, description: "OAuth2 scope (default: all)", default: "all" },
73
+ tenant: { type: "string", required: false, description: "Tenant (cloud only)" },
74
+ });
75
+ registerCrud(rl, "client", "/client");
76
+ registerCrud(rl, "site", "/site");
77
+ registerCrud(rl, "ticket", "/tickets");
78
+ registerCrud(rl, "user", "/users");
79
+ }
@@ -0,0 +1,163 @@
1
+ const BASE_URL = "https://api.harvestapp.com/v2";
2
+ async function apiRequest(token, accountId, method, endpoint, body, qs) {
3
+ const url = new URL(`${BASE_URL}/${endpoint}`);
4
+ if (qs) {
5
+ for (const [k, v] of Object.entries(qs)) {
6
+ if (v !== undefined && v !== null)
7
+ url.searchParams.set(k, String(v));
8
+ }
9
+ }
10
+ const opts = {
11
+ method,
12
+ headers: { Authorization: `Bearer ${token}`, "Harvest-Account-Id": accountId, "Content-Type": "application/json", "User-Agent": "Runline" },
13
+ };
14
+ if (body && Object.keys(body).length > 0 && method !== "GET" && method !== "DELETE")
15
+ opts.body = JSON.stringify(body);
16
+ const res = await fetch(url.toString(), opts);
17
+ if (!res.ok)
18
+ throw new Error(`Harvest API error ${res.status}: ${await res.text()}`);
19
+ if (res.status === 204)
20
+ return { success: true };
21
+ return res.json();
22
+ }
23
+ function getConn(ctx) {
24
+ return { token: ctx.connection.config.token, accountId: ctx.connection.config.accountId };
25
+ }
26
+ function hv(ctx, method, endpoint, body, qs) {
27
+ const { token, accountId } = getConn(ctx);
28
+ return apiRequest(token, accountId, method, endpoint, body, qs);
29
+ }
30
+ function unwrapList(data, key) {
31
+ if (data && typeof data === "object" && key in data)
32
+ return data[key];
33
+ return data;
34
+ }
35
+ function registerCrud(rl, resource, apiPath, listKey) {
36
+ rl.registerAction(`${resource}.create`, {
37
+ description: `Create a ${resource}`, inputSchema: { properties: { type: "object", required: true, description: `${resource} data` } },
38
+ async execute(input, ctx) { return hv(ctx, "POST", apiPath, input.properties); },
39
+ });
40
+ rl.registerAction(`${resource}.get`, {
41
+ description: `Get a ${resource}`, inputSchema: { id: { type: "number", required: true, description: `${resource} ID` } },
42
+ async execute(input, ctx) { return hv(ctx, "GET", `${apiPath}/${input.id}`); },
43
+ });
44
+ rl.registerAction(`${resource}.list`, {
45
+ description: `List ${resource}s`, inputSchema: { limit: { type: "number", required: false, description: "Max results" }, page: { type: "number", required: false, description: "Page" } },
46
+ async execute(input, ctx) { const { limit, page } = (input ?? {}); const qs = {}; if (limit)
47
+ qs.per_page = limit; if (page)
48
+ qs.page = page; return unwrapList(await hv(ctx, "GET", apiPath, undefined, qs), listKey); },
49
+ });
50
+ rl.registerAction(`${resource}.update`, {
51
+ description: `Update a ${resource}`, inputSchema: { id: { type: "number", required: true, description: `${resource} ID` }, properties: { type: "object", required: true, description: "Fields to update" } },
52
+ async execute(input, ctx) { const { id, properties } = input; return hv(ctx, "PATCH", `${apiPath}/${id}`, properties); },
53
+ });
54
+ rl.registerAction(`${resource}.delete`, {
55
+ description: `Delete a ${resource}`, inputSchema: { id: { type: "number", required: true, description: `${resource} ID` } },
56
+ async execute(input, ctx) { await hv(ctx, "DELETE", `${apiPath}/${input.id}`); return { success: true }; },
57
+ });
58
+ }
59
+ export default function harvest(rl) {
60
+ rl.setName("harvest");
61
+ rl.setVersion("0.1.0");
62
+ rl.setConnectionSchema({
63
+ token: { type: "string", required: true, description: "Harvest personal access token", env: "HARVEST_TOKEN" },
64
+ accountId: { type: "string", required: true, description: "Harvest account ID", env: "HARVEST_ACCOUNT_ID" },
65
+ });
66
+ // Standard CRUD resources
67
+ registerCrud(rl, "client", "clients", "clients");
68
+ registerCrud(rl, "project", "projects", "projects");
69
+ registerCrud(rl, "task", "tasks", "tasks");
70
+ registerCrud(rl, "contact", "contacts", "contacts");
71
+ registerCrud(rl, "invoice", "invoices", "invoices");
72
+ registerCrud(rl, "expense", "expenses", "expenses");
73
+ registerCrud(rl, "estimate", "estimates", "estimates");
74
+ // User (CRUD + me)
75
+ registerCrud(rl, "user", "users", "users");
76
+ rl.registerAction("user.me", {
77
+ description: "Get the currently authenticated user",
78
+ async execute(_input, ctx) { return hv(ctx, "GET", "users/me"); },
79
+ });
80
+ // Time entry (special operations)
81
+ rl.registerAction("timeEntry.create", {
82
+ description: "Create a time entry",
83
+ inputSchema: {
84
+ projectId: { type: "number", required: true, description: "Project ID" },
85
+ taskId: { type: "number", required: true, description: "Task ID" },
86
+ spentDate: { type: "string", required: true, description: "Date (YYYY-MM-DD)" },
87
+ hours: { type: "number", required: false, description: "Hours (for duration-based)" },
88
+ startedTime: { type: "string", required: false, description: "Start time HH:MM (for start/end)" },
89
+ endedTime: { type: "string", required: false, description: "End time HH:MM" },
90
+ notes: { type: "string", required: false, description: "Notes" },
91
+ userId: { type: "number", required: false, description: "User ID (admin only)" },
92
+ },
93
+ async execute(input, ctx) {
94
+ const { projectId, taskId, spentDate, hours, startedTime, endedTime, notes, userId } = input;
95
+ const body = { project_id: projectId, task_id: taskId, spent_date: spentDate };
96
+ if (hours !== undefined)
97
+ body.hours = hours;
98
+ if (startedTime)
99
+ body.started_time = startedTime;
100
+ if (endedTime)
101
+ body.ended_time = endedTime;
102
+ if (notes)
103
+ body.notes = notes;
104
+ if (userId)
105
+ body.user_id = userId;
106
+ return hv(ctx, "POST", "time_entries", body);
107
+ },
108
+ });
109
+ rl.registerAction("timeEntry.get", {
110
+ description: "Get a time entry", inputSchema: { id: { type: "number", required: true, description: "Time entry ID" } },
111
+ async execute(input, ctx) { return hv(ctx, "GET", `time_entries/${input.id}`); },
112
+ });
113
+ rl.registerAction("timeEntry.list", {
114
+ description: "List time entries",
115
+ inputSchema: {
116
+ limit: { type: "number", required: false, description: "Max results" },
117
+ page: { type: "number", required: false, description: "Page" },
118
+ from: { type: "string", required: false, description: "From date (YYYY-MM-DD)" },
119
+ to: { type: "string", required: false, description: "To date (YYYY-MM-DD)" },
120
+ userId: { type: "number", required: false, description: "Filter by user" },
121
+ projectId: { type: "number", required: false, description: "Filter by project" },
122
+ },
123
+ async execute(input, ctx) {
124
+ const { limit, page, from, to, userId, projectId } = (input ?? {});
125
+ const qs = {};
126
+ if (limit)
127
+ qs.per_page = limit;
128
+ if (page)
129
+ qs.page = page;
130
+ if (from)
131
+ qs.from = from;
132
+ if (to)
133
+ qs.to = to;
134
+ if (userId)
135
+ qs.user_id = userId;
136
+ if (projectId)
137
+ qs.project_id = projectId;
138
+ return unwrapList(await hv(ctx, "GET", "time_entries", undefined, qs), "time_entries");
139
+ },
140
+ });
141
+ rl.registerAction("timeEntry.update", {
142
+ description: "Update a time entry",
143
+ inputSchema: { id: { type: "number", required: true, description: "Time entry ID" }, properties: { type: "object", required: true, description: "Fields to update" } },
144
+ async execute(input, ctx) { const { id, properties } = input; return hv(ctx, "PATCH", `time_entries/${id}`, properties); },
145
+ });
146
+ rl.registerAction("timeEntry.delete", {
147
+ description: "Delete a time entry", inputSchema: { id: { type: "number", required: true, description: "Time entry ID" } },
148
+ async execute(input, ctx) { await hv(ctx, "DELETE", `time_entries/${input.id}`); return { success: true }; },
149
+ });
150
+ rl.registerAction("timeEntry.restart", {
151
+ description: "Restart a stopped time entry", inputSchema: { id: { type: "number", required: true, description: "Time entry ID" } },
152
+ async execute(input, ctx) { return hv(ctx, "PATCH", `time_entries/${input.id}/restart`); },
153
+ });
154
+ rl.registerAction("timeEntry.stop", {
155
+ description: "Stop a running time entry", inputSchema: { id: { type: "number", required: true, description: "Time entry ID" } },
156
+ async execute(input, ctx) { return hv(ctx, "PATCH", `time_entries/${input.id}/stop`); },
157
+ });
158
+ // Company (read-only)
159
+ rl.registerAction("company.get", {
160
+ description: "Get company info",
161
+ async execute(_input, ctx) { return hv(ctx, "GET", "company"); },
162
+ });
163
+ }
@@ -0,0 +1,176 @@
1
+ const BASE_URL = "https://api.helpscout.net/v2";
2
+ async function apiRequest(token, method, endpoint, body, qs) {
3
+ const url = new URL(`${BASE_URL}${endpoint}`);
4
+ if (qs) {
5
+ for (const [k, v] of Object.entries(qs)) {
6
+ if (v !== undefined && v !== null)
7
+ url.searchParams.set(k, String(v));
8
+ }
9
+ }
10
+ const opts = { method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" } };
11
+ if (body && Object.keys(body).length > 0 && method !== "GET" && method !== "DELETE")
12
+ opts.body = JSON.stringify(body);
13
+ const res = await fetch(url.toString(), opts);
14
+ if (!res.ok)
15
+ throw new Error(`HelpScout API error ${res.status}: ${await res.text()}`);
16
+ if (res.status === 201 || res.status === 204)
17
+ return { success: true, location: res.headers.get("Location") };
18
+ return res.json();
19
+ }
20
+ function unwrapEmbedded(data, key) {
21
+ if (data && typeof data === "object" && "_embedded" in data) {
22
+ return data._embedded[key];
23
+ }
24
+ return data;
25
+ }
26
+ export default function helpscout(rl) {
27
+ rl.setName("helpscout");
28
+ rl.setVersion("0.1.0");
29
+ rl.setConnectionSchema({
30
+ accessToken: { type: "string", required: true, description: "HelpScout OAuth2 access token", env: "HELPSCOUT_ACCESS_TOKEN" },
31
+ });
32
+ const tok = (ctx) => ctx.connection.config.accessToken;
33
+ // ── Conversation ────────────────────────────────────
34
+ rl.registerAction("conversation.create", {
35
+ description: "Create a conversation",
36
+ inputSchema: {
37
+ subject: { type: "string", required: true, description: "Subject" },
38
+ customer: { type: "object", required: true, description: "{email} or {id}" },
39
+ mailboxId: { type: "number", required: true, description: "Mailbox ID" },
40
+ type: { type: "string", required: true, description: "email, phone, chat" },
41
+ threads: { type: "array", required: true, description: "Array of thread objects [{type, text}]" },
42
+ status: { type: "string", required: false, description: "active, pending, closed, spam" },
43
+ tags: { type: "array", required: false, description: "Tag names" },
44
+ },
45
+ async execute(input, ctx) {
46
+ const { subject, customer, mailboxId, type, threads, status, tags } = input;
47
+ const body = { subject, customer, mailboxId, type, threads };
48
+ if (status)
49
+ body.status = status;
50
+ if (tags)
51
+ body.tags = tags;
52
+ return apiRequest(tok(ctx), "POST", "/conversations", body);
53
+ },
54
+ });
55
+ rl.registerAction("conversation.get", {
56
+ description: "Get a conversation",
57
+ inputSchema: { conversationId: { type: "number", required: true, description: "Conversation ID" } },
58
+ async execute(input, ctx) { return apiRequest(tok(ctx), "GET", `/conversations/${input.conversationId}`); },
59
+ });
60
+ rl.registerAction("conversation.list", {
61
+ description: "List conversations",
62
+ inputSchema: {
63
+ mailboxId: { type: "number", required: false, description: "Filter by mailbox" },
64
+ status: { type: "string", required: false, description: "active, pending, closed, spam, all" },
65
+ limit: { type: "number", required: false, description: "Max results" },
66
+ page: { type: "number", required: false, description: "Page" },
67
+ },
68
+ async execute(input, ctx) {
69
+ const { mailboxId, status, limit, page } = (input ?? {});
70
+ const qs = {};
71
+ if (mailboxId)
72
+ qs.mailbox = mailboxId;
73
+ if (status)
74
+ qs.status = status;
75
+ if (limit)
76
+ qs.pageSize = limit;
77
+ if (page)
78
+ qs.page = page;
79
+ return unwrapEmbedded(await apiRequest(tok(ctx), "GET", "/conversations", undefined, qs), "conversations");
80
+ },
81
+ });
82
+ rl.registerAction("conversation.delete", {
83
+ description: "Delete a conversation",
84
+ inputSchema: { conversationId: { type: "number", required: true, description: "Conversation ID" } },
85
+ async execute(input, ctx) { await apiRequest(tok(ctx), "DELETE", `/conversations/${input.conversationId}`); return { success: true }; },
86
+ });
87
+ // ── Customer ────────────────────────────────────────
88
+ rl.registerAction("customer.create", {
89
+ description: "Create a customer",
90
+ inputSchema: {
91
+ firstName: { type: "string", required: true, description: "First name" },
92
+ lastName: { type: "string", required: false, description: "Last name" },
93
+ emails: { type: "array", required: false, description: "Array of {type, value} email objects" },
94
+ phones: { type: "array", required: false, description: "Phone objects" },
95
+ },
96
+ async execute(input, ctx) {
97
+ const { firstName, lastName, emails, phones } = input;
98
+ const body = { firstName };
99
+ if (lastName)
100
+ body.lastName = lastName;
101
+ if (emails)
102
+ body.emails = emails;
103
+ if (phones)
104
+ body.phones = phones;
105
+ return apiRequest(tok(ctx), "POST", "/customers", body);
106
+ },
107
+ });
108
+ rl.registerAction("customer.get", {
109
+ description: "Get a customer",
110
+ inputSchema: { customerId: { type: "number", required: true, description: "Customer ID" } },
111
+ async execute(input, ctx) { return apiRequest(tok(ctx), "GET", `/customers/${input.customerId}`); },
112
+ });
113
+ rl.registerAction("customer.list", {
114
+ description: "List customers",
115
+ inputSchema: { limit: { type: "number", required: false, description: "Max results" }, page: { type: "number", required: false, description: "Page" } },
116
+ async execute(input, ctx) {
117
+ const { limit, page } = (input ?? {});
118
+ const qs = {};
119
+ if (limit)
120
+ qs.pageSize = limit;
121
+ if (page)
122
+ qs.page = page;
123
+ return unwrapEmbedded(await apiRequest(tok(ctx), "GET", "/customers", undefined, qs), "customers");
124
+ },
125
+ });
126
+ rl.registerAction("customer.update", {
127
+ description: "Update a customer",
128
+ inputSchema: {
129
+ customerId: { type: "number", required: true, description: "Customer ID" },
130
+ properties: { type: "object", required: true, description: "Fields to update" },
131
+ },
132
+ async execute(input, ctx) {
133
+ const { customerId, properties } = input;
134
+ return apiRequest(tok(ctx), "PUT", `/customers/${customerId}`, properties);
135
+ },
136
+ });
137
+ rl.registerAction("customer.getProperties", {
138
+ description: "Get custom properties for a customer",
139
+ inputSchema: { customerId: { type: "number", required: true, description: "Customer ID" } },
140
+ async execute(input, ctx) { return apiRequest(tok(ctx), "GET", `/customers/${input.customerId}/properties`); },
141
+ });
142
+ // ── Mailbox ─────────────────────────────────────────
143
+ rl.registerAction("mailbox.get", {
144
+ description: "Get a mailbox",
145
+ inputSchema: { mailboxId: { type: "number", required: true, description: "Mailbox ID" } },
146
+ async execute(input, ctx) { return apiRequest(tok(ctx), "GET", `/mailboxes/${input.mailboxId}`); },
147
+ });
148
+ rl.registerAction("mailbox.list", {
149
+ description: "List mailboxes",
150
+ async execute(_input, ctx) { return unwrapEmbedded(await apiRequest(tok(ctx), "GET", "/mailboxes"), "mailboxes"); },
151
+ });
152
+ // ── Thread ──────────────────────────────────────────
153
+ rl.registerAction("thread.create", {
154
+ description: "Create a thread (reply/note) on a conversation",
155
+ inputSchema: {
156
+ conversationId: { type: "number", required: true, description: "Conversation ID" },
157
+ type: { type: "string", required: true, description: "reply, note, phone, chat" },
158
+ text: { type: "string", required: true, description: "Thread body (HTML)" },
159
+ customer: { type: "object", required: false, description: "Customer {email} or {id}" },
160
+ },
161
+ async execute(input, ctx) {
162
+ const { conversationId, type, text, customer } = input;
163
+ const body = { type, text };
164
+ if (customer)
165
+ body.customer = customer;
166
+ return apiRequest(tok(ctx), "POST", `/conversations/${conversationId}/reply`, body);
167
+ },
168
+ });
169
+ rl.registerAction("thread.list", {
170
+ description: "List threads in a conversation",
171
+ inputSchema: { conversationId: { type: "number", required: true, description: "Conversation ID" } },
172
+ async execute(input, ctx) {
173
+ return unwrapEmbedded(await apiRequest(tok(ctx), "GET", `/conversations/${input.conversationId}/threads`), "threads");
174
+ },
175
+ });
176
+ }