@commandable/integration-data 0.0.6 → 0.0.7

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 (220) hide show
  1. package/dist/credentials-index.d.ts.map +1 -1
  2. package/dist/credentials-index.js +119 -0
  3. package/dist/credentials-index.js.map +1 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/loader.d.ts +33 -0
  9. package/dist/loader.d.ts.map +1 -1
  10. package/dist/loader.js +36 -4
  11. package/dist/loader.js.map +1 -1
  12. package/integrations/__tests__/liveHarness.ts +16 -3
  13. package/integrations/airtable/.env.test +9 -0
  14. package/integrations/airtable/.env.test.example +11 -0
  15. package/integrations/airtable/README.md +27 -0
  16. package/integrations/airtable/__tests__/get_handlers.test.ts +43 -5
  17. package/integrations/confluence/.env.test +25 -0
  18. package/integrations/confluence/.env.test.example +36 -0
  19. package/integrations/confluence/README.md +28 -0
  20. package/integrations/confluence/__tests__/get_handlers.test.ts +121 -0
  21. package/integrations/confluence/__tests__/usage_parity.test.ts +14 -0
  22. package/integrations/confluence/__tests__/write_handlers.test.ts +131 -0
  23. package/integrations/confluence/credentials.json +39 -0
  24. package/integrations/confluence/credentials_hint.md +4 -0
  25. package/integrations/confluence/credentials_hint_api_token.md +9 -0
  26. package/integrations/confluence/credentials_hint_oauth_token.md +8 -0
  27. package/integrations/confluence/handlers/add_comment.js +19 -0
  28. package/integrations/confluence/handlers/add_label.js +16 -0
  29. package/integrations/confluence/handlers/create_page.js +22 -0
  30. package/integrations/confluence/handlers/delete_page.js +17 -0
  31. package/integrations/confluence/handlers/get_comments.js +33 -0
  32. package/integrations/confluence/handlers/get_page_children.js +30 -0
  33. package/integrations/confluence/handlers/get_space.js +22 -0
  34. package/integrations/confluence/handlers/list_spaces.js +39 -0
  35. package/integrations/confluence/handlers/read_page.js +49 -0
  36. package/integrations/confluence/handlers/search_pages.js +42 -0
  37. package/integrations/confluence/handlers/update_page.js +42 -0
  38. package/integrations/confluence/manifest.json +85 -0
  39. package/integrations/confluence/prompt.md +55 -0
  40. package/integrations/confluence/schemas/add_comment.json +22 -0
  41. package/integrations/confluence/schemas/add_label.json +19 -0
  42. package/integrations/confluence/schemas/create_page.json +33 -0
  43. package/integrations/confluence/schemas/delete_page.json +23 -0
  44. package/integrations/confluence/schemas/empty.json +6 -0
  45. package/integrations/confluence/schemas/get_comments.json +24 -0
  46. package/integrations/confluence/schemas/get_page_children.json +28 -0
  47. package/integrations/confluence/schemas/get_space.json +18 -0
  48. package/integrations/confluence/schemas/list_spaces.json +36 -0
  49. package/integrations/confluence/schemas/read_page.json +28 -0
  50. package/integrations/confluence/schemas/search_pages.json +26 -0
  51. package/integrations/confluence/schemas/update_page.json +31 -0
  52. package/integrations/github/.env.test +16 -0
  53. package/integrations/github/.env.test.example +17 -0
  54. package/integrations/github/README.md +75 -0
  55. package/integrations/github/__tests__/get_handlers.test.ts +5 -5
  56. package/integrations/github/__tests__/write_handlers.test.ts +176 -58
  57. package/integrations/github/handlers/create_file.js +46 -0
  58. package/integrations/github/handlers/delete_file.js +14 -1
  59. package/integrations/github/handlers/edit_file.js +52 -0
  60. package/integrations/github/handlers/edit_files.js +107 -0
  61. package/integrations/github/manifest.json +74 -47
  62. package/integrations/github/prompt.md +36 -0
  63. package/integrations/github/schemas/create_file.json +13 -0
  64. package/integrations/github/schemas/delete_file.json +2 -2
  65. package/integrations/github/schemas/edit_file.json +26 -0
  66. package/integrations/github/schemas/edit_files.json +39 -0
  67. package/integrations/google-calendar/.env.test.example +11 -0
  68. package/integrations/google-calendar/README.md +41 -0
  69. package/integrations/google-calendar/__tests__/write_and_admin_handlers.test.ts +7 -7
  70. package/integrations/google-calendar/manifest.json +27 -17
  71. package/integrations/google-docs/README.md +30 -0
  72. package/integrations/google-drive/README.md +26 -0
  73. package/integrations/google-gmail/.env.test.example +11 -0
  74. package/integrations/google-gmail/README.md +49 -0
  75. package/integrations/google-gmail/handlers/create_draft_email.js +1 -1
  76. package/integrations/google-gmail/handlers/read_email.js +1 -1
  77. package/integrations/google-gmail/handlers/send_email.js +1 -1
  78. package/integrations/google-gmail/manifest.json +36 -25
  79. package/integrations/google-sheet/README.md +27 -0
  80. package/integrations/google-slides/README.md +28 -0
  81. package/integrations/hubspot/.env.test.example +20 -0
  82. package/integrations/hubspot/README.md +48 -0
  83. package/integrations/hubspot/__tests__/get_handlers.test.ts +151 -0
  84. package/integrations/hubspot/__tests__/usage_parity.test.ts +10 -0
  85. package/integrations/hubspot/__tests__/write_handlers.test.ts +244 -0
  86. package/integrations/hubspot/credentials.json +48 -0
  87. package/integrations/hubspot/credentials_hint.md +20 -0
  88. package/integrations/hubspot/credentials_hint_oauth_token.md +16 -0
  89. package/integrations/hubspot/handlers/archive_company.js +13 -0
  90. package/integrations/hubspot/handlers/archive_contact.js +13 -0
  91. package/integrations/hubspot/handlers/archive_deal.js +13 -0
  92. package/integrations/hubspot/handlers/archive_ticket.js +13 -0
  93. package/integrations/hubspot/handlers/create_association.js +18 -0
  94. package/integrations/hubspot/handlers/create_company.js +13 -0
  95. package/integrations/hubspot/handlers/create_contact.js +14 -0
  96. package/integrations/hubspot/handlers/create_deal.js +16 -0
  97. package/integrations/hubspot/handlers/create_note.js +44 -0
  98. package/integrations/hubspot/handlers/create_task.js +48 -0
  99. package/integrations/hubspot/handlers/create_ticket.js +15 -0
  100. package/integrations/hubspot/handlers/get_associations.js +14 -0
  101. package/integrations/hubspot/handlers/get_company.js +18 -0
  102. package/integrations/hubspot/handlers/get_contact.js +18 -0
  103. package/integrations/hubspot/handlers/get_deal.js +18 -0
  104. package/integrations/hubspot/handlers/get_ticket.js +20 -0
  105. package/integrations/hubspot/handlers/list_owners.js +12 -0
  106. package/integrations/hubspot/handlers/list_pipelines.js +5 -0
  107. package/integrations/hubspot/handlers/list_properties.js +11 -0
  108. package/integrations/hubspot/handlers/remove_association.js +22 -0
  109. package/integrations/hubspot/handlers/search_companies.js +43 -0
  110. package/integrations/hubspot/handlers/search_contacts.js +43 -0
  111. package/integrations/hubspot/handlers/search_deals.js +43 -0
  112. package/integrations/hubspot/handlers/search_notes.js +43 -0
  113. package/integrations/hubspot/handlers/search_tasks.js +43 -0
  114. package/integrations/hubspot/handlers/search_tickets.js +43 -0
  115. package/integrations/hubspot/handlers/update_company.js +13 -0
  116. package/integrations/hubspot/handlers/update_contact.js +14 -0
  117. package/integrations/hubspot/handlers/update_deal.js +16 -0
  118. package/integrations/hubspot/handlers/update_task.js +17 -0
  119. package/integrations/hubspot/handlers/update_ticket.js +15 -0
  120. package/integrations/hubspot/manifest.json +230 -0
  121. package/integrations/hubspot/prompt.md +69 -0
  122. package/integrations/hubspot/schemas/archive_company.json +13 -0
  123. package/integrations/hubspot/schemas/archive_contact.json +13 -0
  124. package/integrations/hubspot/schemas/archive_deal.json +9 -0
  125. package/integrations/hubspot/schemas/archive_ticket.json +9 -0
  126. package/integrations/hubspot/schemas/create_association.json +24 -0
  127. package/integrations/hubspot/schemas/create_company.json +14 -0
  128. package/integrations/hubspot/schemas/create_contact.json +15 -0
  129. package/integrations/hubspot/schemas/create_deal.json +20 -0
  130. package/integrations/hubspot/schemas/create_note.json +37 -0
  131. package/integrations/hubspot/schemas/create_task.json +51 -0
  132. package/integrations/hubspot/schemas/create_ticket.json +16 -0
  133. package/integrations/hubspot/schemas/empty.json +6 -0
  134. package/integrations/hubspot/schemas/get_associations.json +30 -0
  135. package/integrations/hubspot/schemas/get_company.json +27 -0
  136. package/integrations/hubspot/schemas/get_contact.json +27 -0
  137. package/integrations/hubspot/schemas/get_deal.json +20 -0
  138. package/integrations/hubspot/schemas/get_ticket.json +20 -0
  139. package/integrations/hubspot/schemas/list_owners.json +25 -0
  140. package/integrations/hubspot/schemas/list_pipelines.json +13 -0
  141. package/integrations/hubspot/schemas/list_properties.json +17 -0
  142. package/integrations/hubspot/schemas/remove_association.json +24 -0
  143. package/integrations/hubspot/schemas/search_companies.json +56 -0
  144. package/integrations/hubspot/schemas/search_contacts.json +56 -0
  145. package/integrations/hubspot/schemas/search_deals.json +43 -0
  146. package/integrations/hubspot/schemas/search_notes.json +43 -0
  147. package/integrations/hubspot/schemas/search_tasks.json +43 -0
  148. package/integrations/hubspot/schemas/search_tickets.json +43 -0
  149. package/integrations/hubspot/schemas/update_company.json +20 -0
  150. package/integrations/hubspot/schemas/update_contact.json +21 -0
  151. package/integrations/hubspot/schemas/update_deal.json +19 -0
  152. package/integrations/hubspot/schemas/update_task.json +31 -0
  153. package/integrations/hubspot/schemas/update_ticket.json +18 -0
  154. package/integrations/jira/.env.test +46 -0
  155. package/integrations/jira/.env.test.example +41 -0
  156. package/integrations/jira/README.md +46 -0
  157. package/integrations/jira/__tests__/get_handlers.test.ts +193 -0
  158. package/integrations/jira/__tests__/usage_parity.test.ts +14 -0
  159. package/integrations/jira/__tests__/write_handlers.test.ts +157 -0
  160. package/integrations/jira/credentials.json +39 -0
  161. package/integrations/jira/credentials_hint.md +4 -0
  162. package/integrations/jira/credentials_hint_api_token.md +6 -0
  163. package/integrations/jira/credentials_hint_oauth_token.md +6 -0
  164. package/integrations/jira/handlers/add_comment.js +9 -0
  165. package/integrations/jira/handlers/assign_issue.js +11 -0
  166. package/integrations/jira/handlers/create_issue.js +37 -0
  167. package/integrations/jira/handlers/create_sprint.js +19 -0
  168. package/integrations/jira/handlers/delete_issue.js +10 -0
  169. package/integrations/jira/handlers/get_backlog_issues.js +13 -0
  170. package/integrations/jira/handlers/get_board.js +6 -0
  171. package/integrations/jira/handlers/get_issue.js +63 -0
  172. package/integrations/jira/handlers/get_issue_comments.js +31 -0
  173. package/integrations/jira/handlers/get_myself.js +14 -0
  174. package/integrations/jira/handlers/get_project.js +28 -0
  175. package/integrations/jira/handlers/get_sprint.js +5 -0
  176. package/integrations/jira/handlers/get_sprint_issues.js +13 -0
  177. package/integrations/jira/handlers/get_transitions.js +23 -0
  178. package/integrations/jira/handlers/list_boards.js +34 -0
  179. package/integrations/jira/handlers/list_projects.js +29 -0
  180. package/integrations/jira/handlers/list_sprints.js +29 -0
  181. package/integrations/jira/handlers/move_issues_to_sprint.js +11 -0
  182. package/integrations/jira/handlers/search_issues.js +43 -0
  183. package/integrations/jira/handlers/search_users.js +21 -0
  184. package/integrations/jira/handlers/transition_issue.js +44 -0
  185. package/integrations/jira/handlers/update_issue.js +40 -0
  186. package/integrations/jira/handlers/update_sprint.js +20 -0
  187. package/integrations/jira/manifest.json +204 -0
  188. package/integrations/jira/prompt.md +80 -0
  189. package/integrations/jira/schemas/add_comment.json +16 -0
  190. package/integrations/jira/schemas/assign_issue.json +16 -0
  191. package/integrations/jira/schemas/create_issue.json +49 -0
  192. package/integrations/jira/schemas/create_sprint.json +29 -0
  193. package/integrations/jira/schemas/delete_issue.json +12 -0
  194. package/integrations/jira/schemas/empty.json +6 -0
  195. package/integrations/jira/schemas/get_backlog_issues.json +33 -0
  196. package/integrations/jira/schemas/get_board.json +13 -0
  197. package/integrations/jira/schemas/get_issue.json +23 -0
  198. package/integrations/jira/schemas/get_issue_comments.json +23 -0
  199. package/integrations/jira/schemas/get_project.json +17 -0
  200. package/integrations/jira/schemas/get_sprint.json +13 -0
  201. package/integrations/jira/schemas/get_sprint_issues.json +33 -0
  202. package/integrations/jira/schemas/get_transitions.json +12 -0
  203. package/integrations/jira/schemas/list_boards.json +27 -0
  204. package/integrations/jira/schemas/list_projects.json +22 -0
  205. package/integrations/jira/schemas/list_sprints.json +29 -0
  206. package/integrations/jira/schemas/move_issues_to_sprint.json +19 -0
  207. package/integrations/jira/schemas/search_issues.json +28 -0
  208. package/integrations/jira/schemas/search_users.json +18 -0
  209. package/integrations/jira/schemas/transition_issue.json +38 -0
  210. package/integrations/jira/schemas/update_issue.json +47 -0
  211. package/integrations/jira/schemas/update_sprint.json +33 -0
  212. package/integrations/new_integration_prompt.md +173 -2
  213. package/integrations/notion/.env.test +10 -0
  214. package/integrations/notion/.env.test.example +13 -0
  215. package/integrations/notion/README.md +42 -0
  216. package/integrations/notion/manifest.json +64 -35
  217. package/integrations/trello/.env.test +6 -0
  218. package/integrations/trello/.env.test.example +9 -0
  219. package/integrations/trello/README.md +50 -0
  220. package/package.json +7 -3
@@ -3,19 +3,19 @@ import { createCredentialStore, createIntegrationNode, createProxy, createToolbo
3
3
 
4
4
  // LIVE GitHub read tests -- runs once per available credential variant.
5
5
  // Required env vars (at least one):
6
- // - GITHUB_CLASSIC_PAT
7
- // - GITHUB_FINE_GRAINED_PAT
6
+ // - _GITHUB_CLASSIC_PAT
7
+ // - _GITHUB_FINE_GRAINED_PAT
8
8
 
9
9
  const env = process.env as Record<string, string | undefined>
10
10
 
11
11
  interface VariantConfig {
12
- key: string
12
+ key: string
13
13
  token: string
14
14
  }
15
15
 
16
16
  const variants: VariantConfig[] = [
17
- { key: 'classic_pat', token: env.GITHUB_CLASSIC_PAT || '' },
18
- { key: 'fine_grained_pat', token: env.GITHUB_FINE_GRAINED_PAT || '' },
17
+ { key: 'classic_pat', token: env._GITHUB_CLASSIC_PAT || '' },
18
+ { key: 'fine_grained_pat', token: env._GITHUB_FINE_GRAINED_PAT || '' },
19
19
  ].filter(v => v.token.trim().length > 0)
20
20
 
21
21
  const suiteOrSkip = variants.length > 0 ? describe : describe.skip
@@ -3,11 +3,11 @@ import { createCredentialStore, createIntegrationNode, createProxy, createToolbo
3
3
 
4
4
  // LIVE GitHub write tests -- runs once per available credential variant.
5
5
  // Required env vars (at least one):
6
- // - GITHUB_CLASSIC_PAT (tests all write tools including create_repo/delete_repo)
7
- // - GITHUB_FINE_GRAINED_PAT (tests write tools; create_repo/delete_repo are excluded for this variant)
6
+ // - _GITHUB_CLASSIC_PAT (tests all write tools including create_repo/delete_repo)
7
+ // - _GITHUB_FINE_GRAINED_PAT (tests write tools; create_repo/delete_repo are excluded for this variant)
8
8
  // Plus:
9
- // - GITHUB_TEST_OWNER
10
- // - GITHUB_TEST_REPO
9
+ // - _GITHUB_TEST_OWNER
10
+ // - _GITHUB_TEST_REPO
11
11
 
12
12
  const env = process.env as Record<string, string | undefined>
13
13
 
@@ -17,11 +17,11 @@ interface VariantConfig {
17
17
  }
18
18
 
19
19
  const variants: VariantConfig[] = [
20
- { key: 'classic_pat', token: env.GITHUB_CLASSIC_PAT || '' },
21
- { key: 'fine_grained_pat', token: env.GITHUB_FINE_GRAINED_PAT || '' },
20
+ { key: 'classic_pat', token: env._GITHUB_CLASSIC_PAT || '' },
21
+ { key: 'fine_grained_pat', token: env._GITHUB_FINE_GRAINED_PAT || '' },
22
22
  ].filter(v => v.token.trim().length > 0)
23
23
 
24
- const hasWriteEnv = hasEnv('GITHUB_TEST_OWNER', 'GITHUB_TEST_REPO')
24
+ const hasWriteEnv = hasEnv('_GITHUB_TEST_OWNER', '_GITHUB_TEST_REPO')
25
25
  const suiteOrSkip = (variants.length > 0 && hasWriteEnv) ? describe : describe.skip
26
26
 
27
27
  async function withRetry<T>(
@@ -49,8 +49,8 @@ suiteOrSkip('github write handlers (live)', () => {
49
49
  for (const variant of variants) {
50
50
  describe(`variant: ${variant.key}`, () => {
51
51
  const ctx = {
52
- owner: env.GITHUB_TEST_OWNER,
53
- repo: env.GITHUB_TEST_REPO,
52
+ owner: env._GITHUB_TEST_OWNER,
53
+ repo: env._GITHUB_TEST_REPO,
54
54
  }
55
55
  let toolbox: ReturnType<typeof createToolbox>
56
56
 
@@ -158,97 +158,188 @@ suiteOrSkip('github write handlers (live)', () => {
158
158
  expect(deleted?.status).toBe(204)
159
159
  }, 90000)
160
160
 
161
- it('create_or_update_file: single file commit', async () => {
161
+ it('create_file: create, overwrite, and verify', async () => {
162
162
  if (!ctx.owner || !ctx.repo)
163
163
  return expect(true).toBe(true)
164
164
 
165
165
  const timestamp = Date.now()
166
- const branchName = `test-single-file-${timestamp}`
166
+ const branchName = `test-create-file-${timestamp}`
167
+ const filePath = `test-create-${timestamp}.txt`
167
168
 
168
169
  const create_branch = toolbox.write('create_branch')
169
170
  const branch = await create_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
170
171
  expect(branch?.ref).toBe(`refs/heads/${branchName}`)
171
172
 
172
- const create_or_update_file = toolbox.write('create_or_update_file')
173
- const file = await create_or_update_file({
173
+ const create_file = toolbox.write('create_file')
174
+ const created = await create_file({
174
175
  owner: ctx.owner,
175
176
  repo: ctx.repo,
176
- path: `test-single-${timestamp}.txt`,
177
- message: `Add single test file ${timestamp}`,
178
- content: `Test content with UTF-8: Hello 世界 🌍\nCreated at ${timestamp}`,
179
177
  branch: branchName,
178
+ path: filePath,
179
+ content: `Test content with UTF-8: Hello 世界 🌍\nCreated at ${timestamp}`,
180
+ message: `Add test file ${timestamp}`,
180
181
  })
181
- expect(file?.commit?.message).toBe(`Add single test file ${timestamp}`)
182
- expect(file?.content?.path).toBe(`test-single-${timestamp}.txt`)
182
+ expect(created?.commit?.sha).toBeTruthy()
183
+ expect(created?.file?.path).toBe(filePath)
184
+ expect(created?.file?.action).toBe('created')
185
+
186
+ // Overwrite the same file
187
+ const overwritten = await withRetry(
188
+ () => create_file({
189
+ owner: ctx.owner,
190
+ repo: ctx.repo,
191
+ branch: branchName,
192
+ path: filePath,
193
+ content: `Overwritten content at ${timestamp}`,
194
+ message: `Overwrite test file ${timestamp}`,
195
+ }),
196
+ { maxAttempts: 3, delayMs: 2000 },
197
+ )
198
+ expect(overwritten?.commit?.sha).toBeTruthy()
199
+ expect(overwritten?.file?.action).toBe('overwritten')
183
200
 
184
- // get_file_contents and delete_file roundtrip
201
+ // Verify content -- retry until the Contents API reflects the overwrite
185
202
  const get_file_contents = toolbox.read('get_file_contents')
186
- const contents = await get_file_contents({
187
- owner: ctx.owner,
188
- repo: ctx.repo,
189
- path: `test-single-${timestamp}.txt`,
190
- ref: branchName,
191
- })
192
- expect(contents?.encoding).toBe('utf-8')
193
- expect(contents?.content).toContain('Hello')
194
- const fileSha = contents?.sha
203
+ const contents = await withRetry(
204
+ async () => {
205
+ const c = await get_file_contents({
206
+ owner: ctx.owner,
207
+ repo: ctx.repo,
208
+ path: filePath,
209
+ ref: branchName,
210
+ })
211
+ if (!c?.content?.includes('Overwritten content'))
212
+ throw new Error('stale content: overwrite not yet visible')
213
+ return c
214
+ },
215
+ { maxAttempts: 5, delayMs: 1500 },
216
+ )
217
+ expect(contents?.content).toContain('Overwritten content')
195
218
 
219
+ // delete_file without SHA (auto-fetches it)
196
220
  const delete_file = toolbox.write('delete_file')
197
221
  const deleted = await delete_file({
198
222
  owner: ctx.owner,
199
223
  repo: ctx.repo,
200
- path: `test-single-${timestamp}.txt`,
224
+ path: filePath,
201
225
  message: `Delete test file ${timestamp}`,
202
- sha: fileSha,
203
226
  branch: branchName,
204
227
  })
205
228
  expect(deleted?.commit?.message).toBe(`Delete test file ${timestamp}`)
206
229
 
207
- // delete_branch cleanup
208
230
  const delete_branch = toolbox.write('delete_branch')
209
- const deletedBranch = await delete_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
210
- expect(deletedBranch?.success).toBe(true)
231
+ await delete_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
232
+ }, 120000)
233
+
234
+ it('edit_file: search/replace on a single file', async () => {
235
+ if (!ctx.owner || !ctx.repo)
236
+ return expect(true).toBe(true)
237
+
238
+ const timestamp = Date.now()
239
+ const branchName = `test-edit-file-${timestamp}`
240
+ const filePath = `test-edit-${timestamp}.txt`
241
+
242
+ const create_branch = toolbox.write('create_branch')
243
+ await create_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
244
+
245
+ // Seed a file to edit
246
+ const create_file = toolbox.write('create_file')
247
+ await create_file({
248
+ owner: ctx.owner,
249
+ repo: ctx.repo,
250
+ branch: branchName,
251
+ path: filePath,
252
+ content: 'line 1: hello world\nline 2: foo bar\nline 3: goodbye world\n',
253
+ message: `Seed file for edit test ${timestamp}`,
254
+ })
255
+
256
+ // Apply search/replace edits
257
+ const edit_file = toolbox.write('edit_file')
258
+ const edited = await withRetry(
259
+ () => edit_file({
260
+ owner: ctx.owner,
261
+ repo: ctx.repo,
262
+ branch: branchName,
263
+ path: filePath,
264
+ edits: [
265
+ { old_text: 'hello world', new_text: 'hello universe' },
266
+ { old_text: 'foo bar', new_text: 'baz qux' },
267
+ ],
268
+ message: `Edit file ${timestamp}`,
269
+ }),
270
+ { maxAttempts: 3, delayMs: 2000 },
271
+ )
272
+ expect(edited?.commit?.sha).toBeTruthy()
273
+ expect(edited?.file?.path).toBe(filePath)
274
+
275
+ // Verify edits applied -- retry until the Contents API reflects the new commit
276
+ const get_file_contents = toolbox.read('get_file_contents')
277
+ const contents = await withRetry(
278
+ async () => {
279
+ const c = await get_file_contents({
280
+ owner: ctx.owner,
281
+ repo: ctx.repo,
282
+ path: filePath,
283
+ ref: branchName,
284
+ })
285
+ if (!c?.content?.includes('hello universe'))
286
+ throw new Error('stale content: edit not yet visible')
287
+ return c
288
+ },
289
+ { maxAttempts: 5, delayMs: 1500 },
290
+ )
291
+ expect(contents?.content).toContain('hello universe')
292
+ expect(contents?.content).toContain('baz qux')
293
+ expect(contents?.content).toContain('goodbye world')
294
+ expect(contents?.content).not.toContain('hello world')
295
+
296
+ const delete_branch = toolbox.write('delete_branch')
297
+ await delete_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
211
298
  }, 120000)
212
299
 
213
- it('create_commit: multiple files in one commit', async () => {
300
+ it('edit_files: mixed create, edit, delete in one atomic commit', async () => {
214
301
  if (!ctx.owner || !ctx.repo)
215
302
  return expect(true).toBe(true)
216
303
 
217
304
  const timestamp = Date.now()
218
- const branchName = `test-multi-file-${timestamp}`
305
+ const branchName = `test-edit-files-${timestamp}`
219
306
 
220
307
  const create_branch = toolbox.write('create_branch')
221
308
  const branch = await create_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
222
309
  expect(branch?.ref).toBe(`refs/heads/${branchName}`)
223
310
 
224
- const create_commit = toolbox.write('create_commit')
225
- const commit = await create_commit({
311
+ // Seed initial files via edit_files (create action)
312
+ const edit_files = toolbox.write('edit_files')
313
+ const commit1 = await edit_files({
226
314
  owner: ctx.owner,
227
315
  repo: ctx.repo,
228
316
  branch: branchName,
229
- message: `Add multiple files ${timestamp}`,
317
+ message: `Add initial files ${timestamp}`,
230
318
  files: [
231
- { path: `multi-test/file1-${timestamp}.txt`, content: 'Content of file 1' },
232
- { path: `multi-test/file2-${timestamp}.txt`, content: 'Content of file 2' },
233
- { path: `multi-test/file3-${timestamp}.md`, content: '# Test File 3\n\nWith UTF-8: 你好 🚀' },
319
+ { path: `multi-test/file1-${timestamp}.txt`, action: 'create', content: 'Content of file 1' },
320
+ { path: `multi-test/file2-${timestamp}.txt`, action: 'create', content: 'Content of file 2' },
321
+ { path: `multi-test/file3-${timestamp}.md`, action: 'create', content: '# Test File 3\n\nWith UTF-8: 你好 🚀' },
234
322
  ],
235
323
  })
236
- expect(commit?.commit?.sha).toBeTruthy()
237
- expect(commit?.commit?.message).toBe(`Add multiple files ${timestamp}`)
238
- expect(commit?.files?.length).toBe(3)
324
+ expect(commit1?.commit?.sha).toBeTruthy()
325
+ expect(commit1?.commit?.message).toBe(`Add initial files ${timestamp}`)
326
+ expect(commit1?.files?.length).toBe(3)
239
327
 
240
- // Retry second commit: GitHub's backend sometimes needs a moment to settle
241
- // after the first commit before accepting a follow-up on the same branch.
328
+ // Mixed operations: edit file1, delete file2, create file4
242
329
  const commit2 = await withRetry(
243
- () => create_commit({
330
+ () => edit_files({
244
331
  owner: ctx.owner,
245
332
  repo: ctx.repo,
246
333
  branch: branchName,
247
- message: `Update and delete files ${timestamp}`,
334
+ message: `Mixed edit/delete/create ${timestamp}`,
248
335
  files: [
249
- { path: `multi-test/file1-${timestamp}.txt`, content: 'Updated content of file 1' },
250
- { path: `multi-test/file2-${timestamp}.txt` },
251
- { path: `multi-test/file4-${timestamp}.txt`, content: 'New file 4' },
336
+ {
337
+ path: `multi-test/file1-${timestamp}.txt`,
338
+ action: 'edit',
339
+ edits: [{ old_text: 'Content of file 1', new_text: 'Updated content of file 1' }],
340
+ },
341
+ { path: `multi-test/file2-${timestamp}.txt`, action: 'delete' },
342
+ { path: `multi-test/file4-${timestamp}.txt`, action: 'create', content: 'New file 4' },
252
343
  ],
253
344
  }),
254
345
  {
@@ -258,15 +349,42 @@ suiteOrSkip('github write handlers (live)', () => {
258
349
  },
259
350
  )
260
351
  expect(commit2?.commit?.sha).toBeTruthy()
261
- expect(commit2?.commit?.message).toBe(`Update and delete files ${timestamp}`)
352
+ expect(commit2?.files?.length).toBe(3)
353
+
354
+ // Verify the edit applied -- retry until the Contents API reflects the new commit
355
+ const get_file_contents = toolbox.read('get_file_contents')
356
+ const f1 = await withRetry(
357
+ async () => {
358
+ const c = await get_file_contents({
359
+ owner: ctx.owner,
360
+ repo: ctx.repo,
361
+ path: `multi-test/file1-${timestamp}.txt`,
362
+ ref: branchName,
363
+ })
364
+ if (!c?.content?.includes('Updated content of file 1'))
365
+ throw new Error('stale content: edit not yet visible')
366
+ return c
367
+ },
368
+ { maxAttempts: 5, delayMs: 1500 },
369
+ )
370
+ expect(f1?.content).toContain('Updated content of file 1')
371
+
372
+ // Verify the new file was created
373
+ const f4 = await get_file_contents({
374
+ owner: ctx.owner,
375
+ repo: ctx.repo,
376
+ path: `multi-test/file4-${timestamp}.txt`,
377
+ ref: branchName,
378
+ })
379
+ expect(f4?.content).toContain('New file 4')
262
380
 
263
381
  // get_commit verifies the commit details
264
382
  const get_commit = toolbox.read('get_commit')
265
383
  const commitDetails = await get_commit({ owner: ctx.owner, repo: ctx.repo, sha: commit2.commit.sha })
266
384
  expect(commitDetails?.sha).toBe(commit2.commit.sha)
267
- }, 120000)
385
+ }, 150000)
268
386
 
269
- it('full PR workflow: create_branch -> create_commit -> create_pull_request -> update_pull_request -> create_pull_request_review -> merge_pull_request -> delete_branch', async () => {
387
+ it('full PR workflow: create_branch -> edit_files -> create_pull_request -> update_pull_request -> create_pull_request_review -> merge_pull_request -> delete_branch', async () => {
270
388
  if (!ctx.owner || !ctx.repo)
271
389
  return expect(true).toBe(true)
272
390
 
@@ -277,15 +395,15 @@ suiteOrSkip('github write handlers (live)', () => {
277
395
  const branch = await create_branch({ owner: ctx.owner, repo: ctx.repo, branch: branchName })
278
396
  expect(branch?.ref).toBe(`refs/heads/${branchName}`)
279
397
 
280
- const create_commit = toolbox.write('create_commit')
281
- const commit = await create_commit({
398
+ const edit_files = toolbox.write('edit_files')
399
+ const commit = await edit_files({
282
400
  owner: ctx.owner,
283
401
  repo: ctx.repo,
284
402
  branch: branchName,
285
403
  message: `Add feature files ${timestamp}`,
286
404
  files: [
287
- { path: `feature-${timestamp}/index.js`, content: 'export default function() { return "Hello"; }' },
288
- { path: `feature-${timestamp}/README.md`, content: `# Feature ${timestamp}\n\nThis is a test feature.` },
405
+ { path: `feature-${timestamp}/index.js`, action: 'create', content: 'export default function() { return "Hello"; }' },
406
+ { path: `feature-${timestamp}/README.md`, action: 'create', content: `# Feature ${timestamp}\n\nThis is a test feature.` },
289
407
  ],
290
408
  })
291
409
  expect(commit?.commit?.sha).toBeTruthy()
@@ -0,0 +1,46 @@
1
+ async (input) => {
2
+ const { owner, repo, branch, path, content, message } = input
3
+
4
+ // Check if the file already exists to get its SHA for overwrite
5
+ let existingSha
6
+ try {
7
+ const params = new URLSearchParams()
8
+ params.set('ref', branch)
9
+ const checkRes = await integration.fetch(`/repos/${owner}/${repo}/contents/${path}?${params.toString()}`)
10
+ const checkData = await checkRes.json()
11
+ if (checkData && checkData.sha) {
12
+ existingSha = checkData.sha
13
+ }
14
+ } catch (e) {
15
+ // 404 means file doesn't exist yet -- that's fine
16
+ }
17
+
18
+ const contentBase64 = btoa(unescape(encodeURIComponent(content)))
19
+
20
+ const body = {
21
+ message: message,
22
+ content: contentBase64,
23
+ branch: branch,
24
+ }
25
+ if (existingSha) {
26
+ body.sha = existingSha
27
+ }
28
+
29
+ const res = await integration.fetch(`/repos/${owner}/${repo}/contents/${path}`, {
30
+ method: 'PUT',
31
+ body: body,
32
+ })
33
+ const result = await res.json()
34
+
35
+ return {
36
+ commit: {
37
+ sha: result.commit?.sha,
38
+ message: result.commit?.message,
39
+ url: result.commit?.html_url,
40
+ },
41
+ file: {
42
+ path: path,
43
+ action: existingSha ? 'overwritten' : 'created',
44
+ },
45
+ }
46
+ }
@@ -1,5 +1,18 @@
1
1
  async (input) => {
2
- const body = { message: input.message, sha: input.sha }
2
+ let sha = input.sha
3
+ if (!sha) {
4
+ const params = new URLSearchParams()
5
+ if (input.branch) params.set('ref', input.branch)
6
+ const query = params.toString() ? `?${params.toString()}` : ''
7
+ const fileRes = await integration.fetch(`/repos/${input.owner}/${input.repo}/contents/${input.path}${query}`)
8
+ const fileData = await fileRes.json()
9
+ if (!fileData || !fileData.sha) {
10
+ throw new Error(`File not found: ${input.path}. Cannot delete a file that does not exist.`)
11
+ }
12
+ sha = fileData.sha
13
+ }
14
+
15
+ const body = { message: input.message, sha: sha }
3
16
  if (input.branch) body.branch = input.branch
4
17
  const res = await integration.fetch(
5
18
  `/repos/${input.owner}/${input.repo}/contents/${input.path}`,
@@ -0,0 +1,52 @@
1
+ async (input) => {
2
+ const { owner, repo, branch, path, edits, message } = input
3
+
4
+ const params = new URLSearchParams()
5
+ params.set('ref', branch)
6
+ const fileRes = await integration.fetch(`/repos/${owner}/${repo}/contents/${path}?${params.toString()}`)
7
+ const fileData = await fileRes.json()
8
+
9
+ if (!fileData || !fileData.content || !fileData.sha) {
10
+ throw new Error(`File not found: ${path}. Use get_repo_tree to discover file paths.`)
11
+ }
12
+
13
+ const b64 = fileData.content.replace(/\n/g, '')
14
+ let content = decodeURIComponent(
15
+ atob(b64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')
16
+ )
17
+
18
+ for (let i = 0; i < edits.length; i++) {
19
+ const { old_text, new_text } = edits[i]
20
+ const idx = content.indexOf(old_text)
21
+ if (idx === -1) {
22
+ throw new Error(
23
+ `Edit ${i + 1}/${edits.length} failed: old_text not found in ${path}. `
24
+ + 'Ensure the search text matches the file exactly, including whitespace and indentation. '
25
+ + 'Use get_file_contents to verify the current content.'
26
+ )
27
+ }
28
+ content = content.substring(0, idx) + new_text + content.substring(idx + old_text.length)
29
+ }
30
+
31
+ const contentBase64 = btoa(unescape(encodeURIComponent(content)))
32
+
33
+ const res = await integration.fetch(`/repos/${owner}/${repo}/contents/${path}`, {
34
+ method: 'PUT',
35
+ body: {
36
+ message: message,
37
+ content: contentBase64,
38
+ sha: fileData.sha,
39
+ branch: branch,
40
+ },
41
+ })
42
+ const result = await res.json()
43
+
44
+ return {
45
+ commit: {
46
+ sha: result.commit?.sha,
47
+ message: result.commit?.message,
48
+ url: result.commit?.html_url,
49
+ },
50
+ file: { path: path },
51
+ }
52
+ }
@@ -0,0 +1,107 @@
1
+ async (input) => {
2
+ const { owner, repo, branch, message, files } = input
3
+
4
+ // Helper: decode base64 GitHub content to UTF-8
5
+ function decodeContent(b64Raw) {
6
+ const b64 = b64Raw.replace(/\n/g, '')
7
+ return decodeURIComponent(
8
+ atob(b64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')
9
+ )
10
+ }
11
+
12
+ // Helper: apply search/replace edits to text
13
+ function applyEdits(text, edits, filePath) {
14
+ let result = text
15
+ for (let i = 0; i < edits.length; i++) {
16
+ const { old_text, new_text } = edits[i]
17
+ const idx = result.indexOf(old_text)
18
+ if (idx === -1) {
19
+ throw new Error(
20
+ `Edit ${i + 1}/${edits.length} failed on ${filePath}: old_text not found. `
21
+ + 'Ensure the search text matches exactly, including whitespace and indentation. '
22
+ + 'Use get_file_contents to verify the current content.'
23
+ )
24
+ }
25
+ result = result.substring(0, idx) + new_text + result.substring(idx + old_text.length)
26
+ }
27
+ return result
28
+ }
29
+
30
+ // 1. Get the current commit SHA for the branch
31
+ const refRes = await integration.fetch(`/repos/${owner}/${repo}/git/refs/heads/${branch}`)
32
+ const refData = await refRes.json()
33
+ const currentCommitSha = refData.object.sha
34
+
35
+ // 2. Get the current commit to find its tree
36
+ const commitRes = await integration.fetch(`/repos/${owner}/${repo}/git/commits/${currentCommitSha}`)
37
+ const commitData = await commitRes.json()
38
+ const currentTreeSha = commitData.tree.sha
39
+
40
+ // 3. Resolve final content for each file
41
+ const tree = []
42
+ for (const file of files) {
43
+ if (file.action === 'delete') {
44
+ tree.push({ path: file.path, mode: '100644', type: 'blob', sha: null })
45
+ continue
46
+ }
47
+
48
+ let finalContent
49
+ if (file.action === 'edit') {
50
+ if (!file.edits || file.edits.length === 0) {
51
+ throw new Error(`File ${file.path} has action 'edit' but no edits provided.`)
52
+ }
53
+ const params = new URLSearchParams()
54
+ params.set('ref', branch)
55
+ const fileRes = await integration.fetch(`/repos/${owner}/${repo}/contents/${file.path}?${params.toString()}`)
56
+ const fileData = await fileRes.json()
57
+ if (!fileData || !fileData.content) {
58
+ throw new Error(`File not found: ${file.path}. Use get_repo_tree to discover file paths.`)
59
+ }
60
+ const currentContent = decodeContent(fileData.content)
61
+ finalContent = applyEdits(currentContent, file.edits, file.path)
62
+ } else {
63
+ // action === 'create'
64
+ if (file.content === undefined || file.content === null) {
65
+ throw new Error(`File ${file.path} has action 'create' but no content provided.`)
66
+ }
67
+ finalContent = file.content
68
+ }
69
+
70
+ // Create blob
71
+ const blobRes = await integration.fetch(`/repos/${owner}/${repo}/git/blobs`, {
72
+ method: 'POST',
73
+ body: { content: finalContent, encoding: 'utf-8' },
74
+ })
75
+ const blobData = await blobRes.json()
76
+ tree.push({ path: file.path, mode: '100644', type: 'blob', sha: blobData.sha })
77
+ }
78
+
79
+ // 4. Create a new tree
80
+ const treeRes = await integration.fetch(`/repos/${owner}/${repo}/git/trees`, {
81
+ method: 'POST',
82
+ body: { base_tree: currentTreeSha, tree: tree },
83
+ })
84
+ const treeData = await treeRes.json()
85
+
86
+ // 5. Create the commit
87
+ const newCommitRes = await integration.fetch(`/repos/${owner}/${repo}/git/commits`, {
88
+ method: 'POST',
89
+ body: { message: message, tree: treeData.sha, parents: [currentCommitSha] },
90
+ })
91
+ const newCommitData = await newCommitRes.json()
92
+
93
+ // 6. Update the branch reference
94
+ await integration.fetch(`/repos/${owner}/${repo}/git/refs/heads/${branch}`, {
95
+ method: 'PATCH',
96
+ body: { sha: newCommitData.sha },
97
+ })
98
+
99
+ return {
100
+ commit: {
101
+ sha: newCommitData.sha,
102
+ message: newCommitData.message,
103
+ url: newCommitData.html_url,
104
+ },
105
+ files: files.map(f => ({ path: f.path, action: f.action })),
106
+ }
107
+ }