@sideboard-ai/core 0.1.324 → 0.1.326

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 (83) hide show
  1. package/dist/{abletime-CU2UPMJF.js → abletime-RDHYSZL2.js} +5 -4
  2. package/dist/{abletime-5ZY3ZUUO.js → abletime-RYYFGKMU.js} +6 -5
  3. package/dist/{abletime-oauth-7EOSBEFM.js → abletime-oauth-OYGS2DAU.js} +3 -3
  4. package/dist/{abletime-oauth-4TTCR3CT.js → abletime-oauth-QR2QDVMK.js} +3 -3
  5. package/dist/{abletime-rest-KOKE7V4B.js → abletime-rest-BKYKNPTC.js} +3 -3
  6. package/dist/{abletime-rest-3TG4G7LW.js → abletime-rest-QDPTWNS4.js} +3 -3
  7. package/dist/agents/claude-runner.cjs +59 -22
  8. package/dist/agents/claude-runner.js +39 -16
  9. package/dist/agents/cursor-runner.cjs +97 -32
  10. package/dist/agents/cursor-runner.js +74 -24
  11. package/dist/{agents-YK65UDLT.js → agents-HANWZNI6.js} +9 -8
  12. package/dist/{agents-LUSTCMDN.js → agents-QO7GKBYT.js} +12 -11
  13. package/dist/{app-settings-XIF4EC4K.js → app-settings-RXBH7KY4.js} +1 -1
  14. package/dist/{app-settings-I5HM5RMP.js → app-settings-TUVLZNDE.js} +1 -1
  15. package/dist/{chunk-7MQSYPAY.js → chunk-2EOLWHXH.js} +206 -188
  16. package/dist/{chunk-QOS4465Y.js → chunk-3Q4XNYHY.js} +1 -1
  17. package/dist/{chunk-RR4WCGNB.js → chunk-46FECFZU.js} +18 -15
  18. package/dist/{chunk-5GDJSFCT.js → chunk-4H5MWZ6J.js} +2 -2
  19. package/dist/{chunk-EQ52JR3Q.js → chunk-5MXLDDYF.js} +1 -1
  20. package/dist/{chunk-NCHOTN3C.js → chunk-5YCKATG6.js} +4 -3
  21. package/dist/{chunk-ROOZDDFA.js → chunk-75RPUXJS.js} +7 -8
  22. package/dist/{chunk-INBUV7HY.js → chunk-B244TJWC.js} +2 -2
  23. package/dist/{chunk-JH5SMVM5.js → chunk-BBNWKL4I.js} +268 -235
  24. package/dist/{chunk-ODH3XHMI.js → chunk-C7HUCBX7.js} +5 -5
  25. package/dist/{chunk-OIHRC2ZJ.js → chunk-D744HOI5.js} +25 -3
  26. package/dist/chunk-DBKHL7NC.js +214 -0
  27. package/dist/chunk-F6GE7V3Q.js +216 -0
  28. package/dist/{chunk-JE3JLHMO.js → chunk-FWVUGLBG.js} +3 -3
  29. package/dist/{chunk-DF5BM5OY.js → chunk-GP3RLZ6A.js} +207 -186
  30. package/dist/{chunk-FR3SAYUL.js → chunk-JM2TGOHP.js} +5 -5
  31. package/dist/{chunk-Y3WREIPQ.js → chunk-K7M7MIWL.js} +12 -210
  32. package/dist/{chunk-FUFNMUNR.js → chunk-KECHK3ZT.js} +7 -8
  33. package/dist/{chunk-EKU7FQVG.js → chunk-KXRF6RNL.js} +2 -2
  34. package/dist/chunk-LRHUBGEM.js +1259 -0
  35. package/dist/{chunk-MZIZTSR2.js → chunk-MAW7DIG7.js} +2 -2
  36. package/dist/{chunk-ZHGMRBSC.js → chunk-NURZOXGN.js} +4 -4
  37. package/dist/{chunk-QIC2JXHW.js → chunk-OOXRMTU6.js} +1 -1
  38. package/dist/{chunk-5XNT6BCG.js → chunk-OPXEJJBM.js} +1 -1
  39. package/dist/chunk-OYRN356W.js +80 -0
  40. package/dist/{chunk-7PVT73H6.js → chunk-PB6HW7SA.js} +12 -210
  41. package/dist/{chunk-B4CDSSRN.js → chunk-Q3FHTGIW.js} +2 -0
  42. package/dist/{chunk-7D5QIU3N.js → chunk-QG4CYRA3.js} +301 -268
  43. package/dist/{chunk-EJ66LEKH.js → chunk-QPSKQP6E.js} +24 -3
  44. package/dist/{chunk-Q3YJZPQI.js → chunk-RZVOX2MT.js} +3 -3
  45. package/dist/chunk-SO7G6O24.js +1445 -0
  46. package/dist/{chunk-YDUHLWI5.js → chunk-SUZOPJZC.js} +21 -7
  47. package/dist/chunk-TMCK5TLA.js +25 -0
  48. package/dist/chunk-W3ZLE4PO.js +77 -0
  49. package/dist/chunk-WUCAVIIP.js +28 -0
  50. package/dist/{chunk-BYUCNIXK.js → chunk-XLVLMQKN.js} +13 -9
  51. package/dist/{chunk-ZIRQZUZY.js → chunk-YGVGXQA7.js} +3 -3
  52. package/dist/{chunk-NXUN4U5P.js → chunk-YWQF7WAG.js} +2 -2
  53. package/dist/{chunk-IFWGG2M2.js → chunk-ZSHS46V5.js} +2 -0
  54. package/dist/{chunk-74OPNF7U.js → chunk-ZUAYA57W.js} +2 -2
  55. package/dist/{conductor-SPZOTC7W.js → conductor-4GVTHUUB.js} +7 -7
  56. package/dist/{conductor-JFABMXDA.js → conductor-TGWI6ETZ.js} +6 -6
  57. package/dist/{coordinator-prompt-K66RGNWK.js → coordinator-prompt-3E6SIDTM.js} +4 -4
  58. package/dist/{coordinator-prompt-Z6UFHFDH.js → coordinator-prompt-WEYJREV6.js} +4 -4
  59. package/dist/{cursor-recover-TEBFKAZ4.js → cursor-recover-7F46PAJ5.js} +1 -1
  60. package/dist/{cursor-recover-OWPQUHFH.js → cursor-recover-BDIAZIE3.js} +1 -1
  61. package/dist/{global-workspace-A6UO6PJV.js → global-workspace-FPDZKUPF.js} +4 -4
  62. package/dist/{global-workspace-RHEO55B4.js → global-workspace-UIEXAG2B.js} +4 -4
  63. package/dist/index.cjs +21094 -20840
  64. package/dist/index.d.cts +36 -17
  65. package/dist/index.d.ts +36 -17
  66. package/dist/index.js +343 -1678
  67. package/dist/linear-6S7MK5YK.js +69 -0
  68. package/dist/linear-Z2T4KUAQ.js +67 -0
  69. package/dist/linear-list-A37WLUBO.js +6 -0
  70. package/dist/linear-list-Q4M7AZO2.js +8 -0
  71. package/dist/mcp/run-stdio.cjs +15903 -15640
  72. package/dist/mcp/run-stdio.js +67 -1247
  73. package/dist/{optional-cli-RMMS5L6F.js → optional-cli-AANBL4U5.js} +3 -3
  74. package/dist/{optional-cli-Y3Z3GIPZ.js → optional-cli-CA4LL322.js} +4 -4
  75. package/dist/{orchestrator-7VTAUY24.js → orchestrator-7FVDBE7Y.js} +15 -13
  76. package/dist/{orchestrator-2O6OJQLX.js → orchestrator-RQXRZW7Z.js} +14 -12
  77. package/dist/{thread-store-3F7GR64A.js → thread-store-4ZN3DS2W.js} +1 -1
  78. package/dist/{thread-store-JRSL4HQY.js → thread-store-IC4BQXDW.js} +1 -1
  79. package/dist/{workspaces-NRKEXOYR.js → workspaces-LEW4UP56.js} +4 -4
  80. package/dist/{workspaces-42E6AGXD.js → workspaces-QONNLHNR.js} +4 -4
  81. package/dist/{worktree-ZDA2UJ7C.js → worktree-6GIONDQO.js} +3 -3
  82. package/dist/{worktree-4NGXCCUO.js → worktree-KFEJAQ2H.js} +3 -3
  83. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,3 +1,29 @@
1
+ import {
2
+ ableTimeDependencyTarget,
3
+ ableTimeStateIsClosed,
4
+ commentAbleTimeTask,
5
+ createAbleTimeTask,
6
+ downloadAbleTimeAttachment,
7
+ ensureAbleTimeTask,
8
+ getAbleTimeOrientation,
9
+ getAbleTimeTask,
10
+ issueAttachmentForAbleTimeTask,
11
+ listAbleTimeAssignedIssues,
12
+ listAbleTimeProjects,
13
+ listAbleTimeTasks,
14
+ mapAbleTimeTask,
15
+ searchAbleTimeTasks,
16
+ taskUrl,
17
+ toAbleTimeIssueInfo,
18
+ updateAbleTimeTask,
19
+ verifyAbleTimeConnection
20
+ } from "./chunk-PB6HW7SA.js";
21
+ import {
22
+ clearOptionalCliPresenceCache,
23
+ detectOptionalServiceClis,
24
+ installOptionalServiceCli,
25
+ optionalCliPresence
26
+ } from "./chunk-ZUAYA57W.js";
1
27
  import {
2
28
  AGENT_GIT_ACTIONS,
3
29
  BOARD_COLUMN_DEFS,
@@ -79,7 +105,6 @@ import {
79
105
  formatDevAccessHint,
80
106
  formatEntireGitDirective,
81
107
  formatFleetArchive,
82
- formatGitHubSearchUpdatedSince,
83
108
  formatInjectedNoticesForTurn,
84
109
  formatIssueToolsDirective,
85
110
  formatIssueToolsReminder,
@@ -125,7 +150,6 @@ import {
125
150
  isThisProcessDesktopHost,
126
151
  isUnclaimedRunScriptRequest,
127
152
  issueBlockedByIds,
128
- issueMatchesUpdatedSince,
129
153
  issueNeedsWorkspacePick,
130
154
  issueTicketFromThread,
131
155
  joinSteerPrompt,
@@ -153,11 +177,9 @@ import {
153
177
  openStackLayer,
154
178
  packagedDetachedJobPath,
155
179
  parseDurationMs,
156
- parseIssueSince,
157
180
  parseWeeklyDays,
158
181
  pendingInjectedNotices,
159
182
  prependQueuedItem,
160
- previewIssueCommentBody,
161
183
  previewLand,
162
184
  readRunLog,
163
185
  readSetupLog,
@@ -216,7 +238,23 @@ import {
216
238
  worktreeCleanupSettings,
217
239
  writeAgentBlock,
218
240
  writeWorktreeFile
219
- } from "./chunk-DF5BM5OY.js";
241
+ } from "./chunk-GP3RLZ6A.js";
242
+ import {
243
+ OPTIONAL_SERVICES,
244
+ OPTIONAL_SERVICE_IDS,
245
+ connectOptionalService,
246
+ connectedOptionalServices,
247
+ disconnectOptionalService,
248
+ formatOptionalServicesDirective,
249
+ formatOptionalServicesDirectiveForTurn,
250
+ formatOptionalServicesReminder,
251
+ formatOptionalServicesReminderForTurn,
252
+ isOptionalServiceId,
253
+ normalizeServiceOrigin,
254
+ optionalServiceConnected,
255
+ optionalServiceSpec,
256
+ verifyOptionalService
257
+ } from "./chunk-B244TJWC.js";
220
258
  import {
221
259
  LEGACY_PLAN_FILE_REL,
222
260
  PLAN_FILE_NAME,
@@ -232,6 +270,61 @@ import {
232
270
  threadMayPresentPlan,
233
271
  writePlanFile
234
272
  } from "./chunk-PGPSIEMW.js";
273
+ import {
274
+ LINEAR_OAUTH_CANCELLED,
275
+ LINEAR_OAUTH_PORT,
276
+ LINEAR_OAUTH_REDIRECT,
277
+ LINEAR_OAUTH_SCOPES,
278
+ LinearOAuthCancelledError,
279
+ buildLinearIssueFilter,
280
+ commentLinearIssue,
281
+ createLinearIssue,
282
+ createLinearPkce,
283
+ disconnectLinear,
284
+ downloadLinearAttachment,
285
+ flipLinearRelationType,
286
+ getLinearAuthToken,
287
+ getLinearIssue,
288
+ hasBakedLinearOAuth,
289
+ isLinearNoneToken,
290
+ isLinearOAuthCancelled,
291
+ linearAuthorizationHeader,
292
+ linearCycleIsActive,
293
+ linearGraphql,
294
+ linearOAuthAuthorizeUrl,
295
+ linearOAuthCredentials,
296
+ linearRelationCreateInput,
297
+ listLinearAssignedIssues,
298
+ listLinearCommentsSince,
299
+ listLinearIssuesDirect,
300
+ listLinearIssuesFiltered,
301
+ listLinearProjects,
302
+ listLinearTeamCycles,
303
+ listLinearTeams,
304
+ normalizeLinearRelationType,
305
+ resolveLinearCycle,
306
+ resolveLinearLabel,
307
+ resolveLinearProject,
308
+ resolveLinearState,
309
+ resolveLinearTeam,
310
+ rewriteLinearError,
311
+ startLinearOAuth,
312
+ updateLinearIssue,
313
+ validateLinearApiKey
314
+ } from "./chunk-SO7G6O24.js";
315
+ import {
316
+ downloadAuthenticatedFile,
317
+ extractHttpUrls,
318
+ hostMatches,
319
+ issueAttachmentWorktreePath,
320
+ mergeIssueAttachments
321
+ } from "./chunk-DBKHL7NC.js";
322
+ import {
323
+ formatGitHubSearchUpdatedSince,
324
+ issueMatchesUpdatedSince,
325
+ parseIssueSince,
326
+ previewIssueCommentBody
327
+ } from "./chunk-W3ZLE4PO.js";
235
328
  import {
236
329
  BRIGHTSY_MCP_ALLOWED_TOOLS,
237
330
  CLAUDE_MODEL_CATALOG,
@@ -306,7 +399,19 @@ import {
306
399
  userMcpNamesToDisable,
307
400
  worktreeMcpToolNames,
308
401
  writeInjectedMcpConfig
309
- } from "./chunk-JH5SMVM5.js";
402
+ } from "./chunk-BBNWKL4I.js";
403
+ import {
404
+ getAgentSetupInfo,
405
+ installAgent,
406
+ installNpmGlobalPackage,
407
+ listAgentSetupInfo,
408
+ loginAgent,
409
+ loginManagedAccount,
410
+ openInSystemTerminal,
411
+ prepareTerminalCommand,
412
+ resolveLoginCommand,
413
+ whichOnPath
414
+ } from "./chunk-4H5MWZ6J.js";
310
415
  import {
311
416
  brightsyMcpServerName,
312
417
  connectBrightsyTeam,
@@ -317,6 +422,7 @@ import {
317
422
  listConnectedBrightsyTeams,
318
423
  switchBrightsyAccount
319
424
  } from "./chunk-S5CJUIPY.js";
425
+ import "./chunk-TMCK5TLA.js";
320
426
  import {
321
427
  BRIGHTSY_SUMMARIZE_CONTEXT_TOOL,
322
428
  CHARS_PER_CONTEXT_TOKEN,
@@ -437,7 +543,7 @@ import {
437
543
  workspaceAgentChatSummaries,
438
544
  worktreeRunPortsPath,
439
545
  wrapReviewSkillMarkdown
440
- } from "./chunk-RR4WCGNB.js";
546
+ } from "./chunk-46FECFZU.js";
441
547
  import {
442
548
  STDERR_EVENT_MAX_CHARS,
443
549
  STDOUT_EVENT_MAX_CHARS,
@@ -479,7 +585,7 @@ import {
479
585
  visibleToolRowDetail,
480
586
  withEventParentId,
481
587
  withEventsParentId
482
- } from "./chunk-QIC2JXHW.js";
588
+ } from "./chunk-OOXRMTU6.js";
483
589
  import "./chunk-7H655NEW.js";
484
590
  import {
485
591
  ABLETIME_OAUTH_CALLBACK_PATH,
@@ -503,36 +609,10 @@ import {
503
609
  parseAbleTimeOAuthCallbackUrl,
504
610
  resolveAbleTimeOAuthHost,
505
611
  startAbleTimeOAuth
506
- } from "./chunk-Q3YJZPQI.js";
507
- import {
508
- ableTimeDependencyTarget,
509
- ableTimeStateIsClosed,
510
- commentAbleTimeTask,
511
- createAbleTimeTask,
512
- downloadAbleTimeAttachment,
513
- downloadAuthenticatedFile,
514
- ensureAbleTimeTask,
515
- extractHttpUrls,
516
- getAbleTimeOrientation,
517
- getAbleTimeTask,
518
- hostMatches,
519
- issueAttachmentForAbleTimeTask,
520
- issueAttachmentWorktreePath,
521
- listAbleTimeAssignedIssues,
522
- listAbleTimeProjects,
523
- listAbleTimeTasks,
524
- looksLikeHttpUrl,
525
- mapAbleTimeTask,
526
- mergeIssueAttachments,
527
- searchAbleTimeTasks,
528
- taskUrl,
529
- toAbleTimeIssueInfo,
530
- updateAbleTimeTask,
531
- verifyAbleTimeConnection
532
- } from "./chunk-7PVT73H6.js";
612
+ } from "./chunk-RZVOX2MT.js";
533
613
  import {
534
614
  looksLikeAbleTimeUlid
535
- } from "./chunk-EKU7FQVG.js";
615
+ } from "./chunk-KXRF6RNL.js";
536
616
  import {
537
617
  ABLETIME_MCP_PATH,
538
618
  ABLETIME_MCP_PM_PATH,
@@ -553,41 +633,7 @@ import {
553
633
  normalizeAbleTimeCredential,
554
634
  normalizeAbleTimeHost,
555
635
  rewriteAbleTimeError
556
- } from "./chunk-ODH3XHMI.js";
557
- import {
558
- clearOptionalCliPresenceCache,
559
- detectOptionalServiceClis,
560
- installOptionalServiceCli,
561
- optionalCliPresence
562
- } from "./chunk-74OPNF7U.js";
563
- import {
564
- OPTIONAL_SERVICES,
565
- OPTIONAL_SERVICE_IDS,
566
- connectOptionalService,
567
- connectedOptionalServices,
568
- disconnectOptionalService,
569
- formatOptionalServicesDirective,
570
- formatOptionalServicesDirectiveForTurn,
571
- formatOptionalServicesReminder,
572
- formatOptionalServicesReminderForTurn,
573
- isOptionalServiceId,
574
- normalizeServiceOrigin,
575
- optionalServiceConnected,
576
- optionalServiceSpec,
577
- verifyOptionalService
578
- } from "./chunk-INBUV7HY.js";
579
- import {
580
- getAgentSetupInfo,
581
- installAgent,
582
- installNpmGlobalPackage,
583
- listAgentSetupInfo,
584
- loginAgent,
585
- loginManagedAccount,
586
- openInSystemTerminal,
587
- prepareTerminalCommand,
588
- resolveLoginCommand,
589
- whichOnPath
590
- } from "./chunk-5GDJSFCT.js";
636
+ } from "./chunk-C7HUCBX7.js";
591
637
  import {
592
638
  CLOUD_COORDINATOR_BUSY_REPLY,
593
639
  CLOUD_COORDINATOR_CANCELED_REPLY,
@@ -640,7 +686,7 @@ import {
640
686
  stageBuffersAsAttachments,
641
687
  syncWorkspacesFromThreads,
642
688
  takenTeamSlugsForOrchestration
643
- } from "./chunk-JE3JLHMO.js";
689
+ } from "./chunk-FWVUGLBG.js";
644
690
  import {
645
691
  GITHUB_PR_BODY_MAX_CHARS,
646
692
  GITHUB_PR_BODY_SAFE_CHARS,
@@ -729,7 +775,7 @@ import {
729
775
  suggestSlug,
730
776
  switchWorktreeBranch,
731
777
  warmGithubAgentAuth
732
- } from "./chunk-FUFNMUNR.js";
778
+ } from "./chunk-KECHK3ZT.js";
733
779
  import {
734
780
  FAMOUS_SOCCER_TEAMS,
735
781
  allocateTeamName,
@@ -764,7 +810,7 @@ import {
764
810
  worktreeNameFromPath,
765
811
  worktreeSlugForTicket,
766
812
  writeThread
767
- } from "./chunk-IFWGG2M2.js";
813
+ } from "./chunk-ZSHS46V5.js";
768
814
  import "./chunk-QKHEVSB5.js";
769
815
  import {
770
816
  BRIGHTSY_OAUTH_CLIENT_ID,
@@ -802,7 +848,7 @@ import {
802
848
  import {
803
849
  clipMcpToolResult,
804
850
  cursorToolErrorText
805
- } from "./chunk-NCHOTN3C.js";
851
+ } from "./chunk-5YCKATG6.js";
806
852
  import "./chunk-AXQ4ZWHK.js";
807
853
  import {
808
854
  AGENT_RUNNER_MAX_OLD_SPACE_MB,
@@ -820,8 +866,8 @@ import {
820
866
  trustSystemCertificates,
821
867
  turnCostUsdFromCursorUsage,
822
868
  withMaxOldSpaceSize
823
- } from "./chunk-ZHGMRBSC.js";
824
- import "./chunk-5XNT6BCG.js";
869
+ } from "./chunk-NURZOXGN.js";
870
+ import "./chunk-OPXEJJBM.js";
825
871
  import {
826
872
  ATTACHMENTS_DIR,
827
873
  CLI_DIR,
@@ -870,7 +916,7 @@ import {
870
916
  rpcNotify,
871
917
  rpcResult,
872
918
  writeAgentRuntimeMetadata
873
- } from "./chunk-YDUHLWI5.js";
919
+ } from "./chunk-SUZOPJZC.js";
874
920
  import {
875
921
  AGENT_DONE_SOUNDS,
876
922
  BAKED_REMOTE_RELAY_URL,
@@ -1012,6 +1058,7 @@ import {
1012
1058
  showCostEnabled,
1013
1059
  slackAppLevelToken,
1014
1060
  slackListenEnabled,
1061
+ subtractUsage,
1015
1062
  sumUsageList,
1016
1063
  toPublicAppSettings,
1017
1064
  totalTokens,
@@ -1030,7 +1077,7 @@ import {
1030
1077
  withTurnScopedCost,
1031
1078
  writeSecureJson,
1032
1079
  writeViewerContext
1033
- } from "./chunk-EJ66LEKH.js";
1080
+ } from "./chunk-QPSKQP6E.js";
1034
1081
  import {
1035
1082
  THINKING_EFFORTS,
1036
1083
  isThinkingEffort,
@@ -3691,1615 +3738,232 @@ function issueNumberFromApiUrl(url) {
3691
3738
  const match = url.match(/\/issues\/(\d+)(?:\b|$)/i);
3692
3739
  if (!match) return null;
3693
3740
  const number = Number(match[1]);
3694
- return Number.isFinite(number) && number > 0 ? number : null;
3695
- }
3696
- var GITHUB_ISSUE_COMMENTS_PAGE_SIZE = 100;
3697
- var GITHUB_ISSUE_COMMENTS_MAX_PAGES = 5;
3698
- function parseGitHubIssueCommentNode(item, allowed) {
3699
- const rec = item && typeof item === "object" ? item : null;
3700
- if (!rec) return null;
3701
- const issueUrl = typeof rec.issue_url === "string" ? rec.issue_url : "";
3702
- const number = issueNumberFromApiUrl(issueUrl);
3703
- if (number == null) return null;
3704
- if (allowed && !allowed.has(number)) return null;
3705
- const body = previewIssueCommentBody(typeof rec.body === "string" ? rec.body : "");
3706
- const user = rec.user && typeof rec.user === "object" ? String(rec.user.login ?? "").trim() : "";
3707
- const createdAt = typeof rec.created_at === "string" ? rec.created_at : typeof rec.createdAt === "string" ? rec.createdAt : void 0;
3708
- const url = typeof rec.html_url === "string" ? rec.html_url : void 0;
3709
- return {
3710
- identifier: `#${number}`,
3711
- ...user ? { author: user } : {},
3712
- ...createdAt ? { createdAt } : {},
3713
- body,
3714
- ...url ? { url } : {}
3715
- };
3716
- }
3717
- async function listGitHubIssueCommentsSince(opts) {
3718
- const limit = Math.max(1, Math.min(250, opts.limit ?? 200));
3719
- const { cwd, slug } = await resolveGitHubIssueRepo(opts.repoPath);
3720
- if (!slug) return [];
3721
- const restrictToIssues = opts.issueIdentifiers != null;
3722
- const allowed = new Set(
3723
- (opts.issueIdentifiers ?? []).map((id) => {
3724
- try {
3725
- return parseGitHubIssueNumber(id);
3726
- } catch {
3727
- return null;
3728
- }
3729
- }).filter((n) => n != null)
3730
- );
3731
- if (restrictToIssues && allowed.size === 0) return [];
3732
- const pageSize = GITHUB_ISSUE_COMMENTS_PAGE_SIZE;
3733
- const out = [];
3734
- for (let page = 1; page <= GITHUB_ISSUE_COMMENTS_MAX_PAGES; page++) {
3735
- const path2 = `repos/${slug}/issues/comments?since=${encodeURIComponent(opts.since)}&sort=created&direction=desc&per_page=${pageSize}&page=${page}`;
3736
- const result = await gh(["api", path2], cwd, { reject: false });
3737
- if (result.exitCode !== 0 || !result.stdout.trim()) break;
3738
- let parsed;
3739
- try {
3740
- parsed = JSON.parse(result.stdout);
3741
- } catch {
3742
- break;
3743
- }
3744
- if (!Array.isArray(parsed) || parsed.length === 0) break;
3745
- for (const item of parsed) {
3746
- const comment = parseGitHubIssueCommentNode(item, restrictToIssues ? allowed : null);
3747
- if (!comment) continue;
3748
- out.push(comment);
3749
- if (out.length >= limit) return out;
3750
- }
3751
- if (parsed.length < pageSize) break;
3752
- }
3753
- return out;
3754
- }
3755
- async function getGitHubIssue(id, opts) {
3756
- const number = parseGitHubIssueNumber(id);
3757
- const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
3758
- const stdout = requireGhOk(
3759
- await gh(
3760
- [
3761
- "issue",
3762
- "view",
3763
- String(number),
3764
- ...repoArgs,
3765
- "--json",
3766
- "number,title,body,url,state,labels,assignees,comments,author"
3767
- ],
3768
- cwd,
3769
- { reject: false }
3770
- ),
3771
- `GitHub issue ${number}`
3772
- );
3773
- let parsed;
3774
- try {
3775
- parsed = JSON.parse(stdout);
3776
- } catch {
3777
- throw new Error(`GitHub issue ${number}: gh returned non-JSON`);
3778
- }
3779
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3780
- throw new Error(`GitHub issue not found: #${number}`);
3781
- }
3782
- const issue = toGitHubIssue(parsed);
3783
- const projects = await readGitHubIssueProjects(number, cwd, repoArgs);
3784
- issue.projects = projects.projects;
3785
- if (projects.error) issue.projectsError = projects.error;
3786
- return issue;
3787
- }
3788
- async function commentGitHubIssue(input, opts) {
3789
- const number = parseGitHubIssueNumber(input.id);
3790
- const body = input.body.trim();
3791
- if (!body) throw new Error("GitHub comment body is required");
3792
- const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
3793
- const stdout = requireGhOk(
3794
- await gh(
3795
- ["issue", "comment", String(number), ...repoArgs, "--body", body],
3796
- cwd,
3797
- { reject: false }
3798
- ),
3799
- `GitHub comment on #${number}`
3800
- );
3801
- const url = stdout.trim().split(/\s+/).find((part) => /^https?:\/\//i.test(part));
3802
- return { body, url };
3803
- }
3804
- function isGitHubNoneToken(value) {
3805
- if (value == null) return true;
3806
- const s = value.trim();
3807
- return !s || /^(none|clear|unschedule|null)$/i.test(s);
3808
- }
3809
- function githubRelationEditFlag(type, remove = false) {
3810
- const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
3811
- if (s === "blockedby" || s === "blocked" || s === "dependson") {
3812
- return remove ? "--remove-blocked-by" : "--add-blocked-by";
3813
- }
3814
- if (s === "blocks" || s === "blocking") {
3815
- return remove ? "--remove-blocking" : "--add-blocking";
3816
- }
3817
- throw new Error(
3818
- `GitHub relation type must be blocks or blockedBy (got ${type}). Parent is a separate parent= field.`
3819
- );
3820
- }
3821
- function labelsEqual(a, b) {
3822
- return a.trim().toLowerCase() === b.trim().toLowerCase();
3823
- }
3824
- async function updateGitHubIssue(input, opts) {
3825
- const number = parseGitHubIssueNumber(input.id);
3826
- const title = input.title?.trim();
3827
- const body = input.body;
3828
- const state = input.state?.trim().toLowerCase();
3829
- const addRelations = input.relations?.length ? input.relations : void 0;
3830
- const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
3831
- const hasEditFields = Boolean(title) || body !== void 0 || input.labels !== void 0 || input.project !== void 0 || input.parent !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
3832
- if (!hasEditFields && !state) {
3833
- throw new Error(
3834
- "github_update_issue needs at least one of title, body, state, labels, project, parent, relations, removeRelations"
3835
- );
3836
- }
3837
- const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
3838
- if (state === "closed" || state === "close") {
3839
- requireGhOk(
3840
- await gh(["issue", "close", String(number), ...repoArgs], cwd, { reject: false }),
3841
- `GitHub close #${number}`
3842
- );
3843
- } else if (state === "open" || state === "reopen") {
3844
- requireGhOk(
3845
- await gh(["issue", "reopen", String(number), ...repoArgs], cwd, { reject: false }),
3846
- `GitHub reopen #${number}`
3847
- );
3848
- } else if (state) {
3849
- throw new Error(`GitHub issue state must be open or closed (got ${input.state})`);
3850
- }
3851
- const args = ["issue", "edit", String(number), ...repoArgs];
3852
- if (title) args.push("--title", title);
3853
- if (body !== void 0) args.push("--body", body);
3854
- if (input.labels !== void 0) {
3855
- const existing = await getGitHubIssue(String(number), opts);
3856
- const desired = input.labels.map((name) => name.trim()).filter(Boolean);
3857
- const add = desired.filter((name) => !existing.labels.some((cur) => labelsEqual(cur, name)));
3858
- const remove = existing.labels.filter((cur) => !desired.some((name) => labelsEqual(cur, name)));
3859
- if (add.length) args.push("--add-label", add.join(","));
3860
- if (remove.length) args.push("--remove-label", remove.join(","));
3861
- }
3862
- if (input.project !== void 0) {
3863
- const current = await listGitHubIssueProjectNames(number, cwd, repoArgs);
3864
- if (isGitHubNoneToken(input.project)) {
3865
- for (const name of current) args.push("--remove-project", name);
3866
- } else {
3867
- const wanted = (input.project ?? "").trim();
3868
- if (!current.some((name) => labelsEqual(name, wanted))) {
3869
- args.push("--add-project", wanted);
3870
- }
3871
- for (const name of current) {
3872
- if (!labelsEqual(name, wanted)) args.push("--remove-project", name);
3873
- }
3874
- }
3875
- }
3876
- if (input.parent !== void 0) {
3877
- if (isGitHubNoneToken(input.parent)) {
3878
- args.push("--remove-parent");
3879
- } else {
3880
- args.push("--parent", String(parseGitHubIssueNumber(input.parent ?? "")));
3881
- }
3882
- }
3883
- for (const rel of removeRelations ?? []) {
3884
- args.push(githubRelationEditFlag(rel.type, true), String(parseGitHubIssueNumber(rel.issue)));
3885
- }
3886
- for (const rel of addRelations ?? []) {
3887
- args.push(githubRelationEditFlag(rel.type, false), String(parseGitHubIssueNumber(rel.issue)));
3888
- }
3889
- const editChanged = args.length > 3 + repoArgs.length;
3890
- if (editChanged) {
3891
- requireGhOk(await gh(args, cwd, { reject: false }), `GitHub edit #${number}`);
3892
- }
3893
- return getGitHubIssue(String(number), opts);
3894
- }
3895
- async function createGitHubIssue(input, opts) {
3896
- const title = input.title.trim();
3897
- if (!title) throw new Error("GitHub issue title is required");
3898
- const parent = input.parent?.trim();
3899
- const parentNumber = parent ? parseGitHubIssueNumber(parent) : null;
3900
- const bodyParts = [
3901
- parentNumber ? `Spin-off of #${parentNumber}.` : null,
3902
- input.body?.trim() || null
3903
- ].filter(Boolean);
3904
- const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
3905
- const args = ["issue", "create", ...repoArgs, "--title", title];
3906
- if (bodyParts.length) args.push("--body", bodyParts.join("\n\n"));
3907
- const created = await gh([...args, "--json", "number,url,title"], cwd, { reject: false });
3908
- if (created.exitCode === 0 && created.stdout.trim()) {
3909
- try {
3910
- const parsed = JSON.parse(created.stdout);
3911
- if (parsed.number) return getGitHubIssue(String(parsed.number), opts);
3912
- } catch {
3913
- }
3914
- }
3915
- const fallback = created.exitCode === 0 ? created : await gh(args, cwd, { reject: false });
3916
- const stdout = requireGhOk(fallback, "GitHub create issue");
3917
- const url = stdout.trim().match(/https?:\/\/github\.com\/[^/\s]+\/[^/\s]+\/issues\/(\d+)/i);
3918
- if (url?.[1]) return getGitHubIssue(url[1], opts);
3919
- throw new Error(`GitHub create issue: could not parse issue from ${stdout.trim() || "empty output"}`);
3920
- }
3921
-
3922
- // src/integrations/linear-oauth.ts
3923
- import { createHash, randomBytes as randomBytes2 } from "crypto";
3924
- import { createServer } from "http";
3925
-
3926
- // src/integrations/linear-app.ts
3927
- var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
3928
- function hasBakedLinearOAuth() {
3929
- return Boolean(BAKED_LINEAR_CLIENT_ID.trim());
3930
- }
3931
-
3932
- // src/integrations/linear-oauth.ts
3933
- var LINEAR_OAUTH_PORT = 19848;
3934
- var LINEAR_OAUTH_REDIRECT = `http://127.0.0.1:${LINEAR_OAUTH_PORT}/callback`;
3935
- var LINEAR_AUTHORIZE = "https://linear.app/oauth/authorize";
3936
- var LINEAR_TOKEN = "https://api.linear.app/oauth/token";
3937
- var LINEAR_REVOKE = "https://api.linear.app/oauth/revoke";
3938
- var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
3939
- var LINEAR_OAUTH_SCOPES = "read,write";
3940
- var REFRESH_SKEW_MS = 5 * 6e4;
3941
- function linearOAuthCredentials() {
3942
- const settings = loadAppSettings();
3943
- const clientId = process.env.SIDEBOARD_LINEAR_CLIENT_ID?.trim() || settings.integrations.linearClientId?.trim() || BAKED_LINEAR_CLIENT_ID.trim();
3944
- const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || "";
3945
- if (!clientId) {
3946
- throw new Error(
3947
- `Linear browser sign-in needs a Linear OAuth app Client ID. Create one at linear.app/settings/api/applications/new with callback ${LINEAR_OAUTH_REDIRECT}, then set SIDEBOARD_LINEAR_CLIENT_ID (PKCE does not require a secret). You can still paste a personal API key.`
3948
- );
3949
- }
3950
- return { clientId, clientSecret };
3951
- }
3952
- function createLinearPkce() {
3953
- const verifier = randomBytes2(32).toString("base64url");
3954
- const challenge = createHash("sha256").update(verifier).digest("base64url");
3955
- return { verifier, challenge };
3956
- }
3957
- function linearOAuthAuthorizeUrl(clientId, state, codeChallenge) {
3958
- const params = new URLSearchParams({
3959
- client_id: clientId,
3960
- redirect_uri: LINEAR_OAUTH_REDIRECT,
3961
- response_type: "code",
3962
- scope: LINEAR_OAUTH_SCOPES,
3963
- state,
3964
- prompt: "consent",
3965
- actor: "user",
3966
- code_challenge: codeChallenge,
3967
- code_challenge_method: "S256"
3968
- });
3969
- return `${LINEAR_AUTHORIZE}?${params.toString()}`;
3970
- }
3971
- function linearAuthorizationHeader(token) {
3972
- const t = token.trim();
3973
- if (!t) return t;
3974
- if (/^bearer\s+/i.test(t)) return t;
3975
- if (t.startsWith("lin_api_")) return t;
3976
- return `Bearer ${t}`;
3977
- }
3978
- function htmlPage(title, body) {
3979
- return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title>
3980
- <style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:48px 24px;max-width:36rem;margin:0 auto;color:#1a1a1a}
3981
- h1{font-size:1.25rem}p{line-height:1.5;color:#444}</style></head><body>${body}</body></html>`;
3982
- }
3983
- async function exchangeLinearToken(body) {
3984
- const res = await httpFetch(LINEAR_TOKEN, {
3985
- method: "POST",
3986
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
3987
- body
3988
- });
3989
- const data = await res.json().catch(() => ({}));
3990
- if (!res.ok || data.error || !data.access_token?.trim()) {
3991
- const detail = data.error_description || data.error || `HTTP ${res.status}`;
3992
- throw new Error(`Linear OAuth token exchange failed: ${detail}`);
3993
- }
3994
- return data;
3995
- }
3996
- async function persistLinearTokens(data, viewer) {
3997
- return saveLinearOAuth({
3998
- accessToken: data.access_token.trim(),
3999
- refreshToken: data.refresh_token?.trim(),
4000
- expiresIn: data.expires_in,
4001
- viewerName: viewer?.name,
4002
- organizationName: viewer?.organizationName
4003
- });
4004
- }
4005
- async function fetchLinearViewer(accessToken3) {
4006
- const res = await httpFetch(LINEAR_GRAPHQL, {
4007
- method: "POST",
4008
- headers: {
4009
- "Content-Type": "application/json",
4010
- Authorization: linearAuthorizationHeader(accessToken3)
4011
- },
4012
- body: JSON.stringify({
4013
- query: "{ viewer { name organization { name } } }"
4014
- })
4015
- });
4016
- if (!res.ok) return {};
4017
- const json = await res.json().catch(() => ({}));
4018
- const viewer = json.data?.viewer;
4019
- return {
4020
- name: viewer?.name?.trim() || void 0,
4021
- organizationName: viewer?.organization?.name?.trim() || void 0
4022
- };
4023
- }
4024
- var refreshInFlight = null;
4025
- async function refreshLinearAccessToken(refreshToken) {
4026
- const { clientId, clientSecret } = linearOAuthCredentials();
4027
- const body = new URLSearchParams({
4028
- grant_type: "refresh_token",
4029
- refresh_token: refreshToken,
4030
- client_id: clientId
4031
- });
4032
- if (clientSecret) body.set("client_secret", clientSecret);
4033
- try {
4034
- const data = await exchangeLinearToken(body);
4035
- await persistLinearTokens(data);
4036
- return data.access_token.trim();
4037
- } catch (err) {
4038
- const message = err instanceof Error ? err.message : String(err);
4039
- if (/invalid_grant|invalid_token|unauthorized/i.test(message)) {
4040
- disconnectLinearConnection();
4041
- throw new Error("Linear sign-in expired \u2014 connect again in Account settings");
4042
- }
4043
- throw err;
4044
- }
4045
- }
4046
- async function getLinearAuthToken(settings = loadAppSettings()) {
4047
- const access = settings.integrations.linearAccessToken?.trim() || "";
4048
- const refresh = settings.integrations.linearRefreshToken?.trim() || "";
4049
- const expiresAt = settings.integrations.linearTokenExpiresAt ?? 0;
4050
- const apiKey = settings.integrations.linearApiKey?.trim() || "";
4051
- if (access && (!refresh || Date.now() < expiresAt - REFRESH_SKEW_MS)) {
4052
- return access;
4053
- }
4054
- if (refresh) {
4055
- if (!refreshInFlight) {
4056
- refreshInFlight = refreshLinearAccessToken(refresh).finally(() => {
4057
- refreshInFlight = null;
4058
- });
4059
- }
4060
- return refreshInFlight.catch((err) => {
4061
- if (apiKey) return apiKey;
4062
- throw err;
4063
- });
4064
- }
4065
- return apiKey || null;
4066
- }
4067
- var LINEAR_OAUTH_CANCELLED = "Linear sign-in cancelled";
4068
- var LinearOAuthCancelledError = class extends Error {
4069
- constructor(message = LINEAR_OAUTH_CANCELLED) {
4070
- super(message);
4071
- this.name = "LinearOAuthCancelledError";
4072
- }
4073
- };
4074
- function isLinearOAuthCancelled(err) {
4075
- if (err instanceof LinearOAuthCancelledError) return true;
4076
- const message = err instanceof Error ? err.message : String(err);
4077
- if (message.includes(LINEAR_OAUTH_CANCELLED)) return true;
4078
- return err instanceof Error && err.name === "LinearOAuthCancelledError";
4079
- }
4080
- async function startLinearOAuth(opts) {
4081
- if (opts?.signal?.aborted) {
4082
- throw new LinearOAuthCancelledError();
4083
- }
4084
- const { clientId, clientSecret } = linearOAuthCredentials();
4085
- const state = randomBytes2(16).toString("hex");
4086
- const pkce = createLinearPkce();
4087
- const authorizeUrl = linearOAuthAuthorizeUrl(clientId, state, pkce.challenge);
4088
- const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
4089
- const code = await new Promise((resolve, reject) => {
4090
- let settled = false;
4091
- const finish = (err, value) => {
4092
- if (settled) return;
4093
- settled = true;
4094
- cleanup();
4095
- if (err) reject(err);
4096
- else resolve(value);
4097
- };
4098
- const server = createServer((req, res) => {
4099
- try {
4100
- const url = new URL(req.url || "/", `http://127.0.0.1:${LINEAR_OAUTH_PORT}`);
4101
- if (url.pathname !== "/callback") {
4102
- res.writeHead(404);
4103
- res.end("Not found");
4104
- return;
4105
- }
4106
- const err = url.searchParams.get("error");
4107
- const gotState = url.searchParams.get("state");
4108
- const gotCode = url.searchParams.get("code");
4109
- if (err) {
4110
- res.writeHead(400, { "Content-Type": "text/html" });
4111
- res.end(htmlPage("Linear", `<h1>Authorization cancelled</h1><p>${err}</p>`));
4112
- finish(new LinearOAuthCancelledError(`Linear OAuth: ${err}`));
4113
- return;
4114
- }
4115
- if (gotState !== state || !gotCode) {
4116
- res.writeHead(400, { "Content-Type": "text/html" });
4117
- res.end(htmlPage("Linear", "<h1>Invalid callback</h1><p>State or code missing.</p>"));
4118
- finish(new Error("Linear OAuth callback was invalid"));
4119
- return;
4120
- }
4121
- res.writeHead(200, { "Content-Type": "text/html" });
4122
- res.end(
4123
- htmlPage(
4124
- "Linear connected",
4125
- "<h1>Linear connected</h1><p>You can close this tab and return to Sideboard.</p>"
4126
- )
4127
- );
4128
- finish(void 0, gotCode);
4129
- } catch (e) {
4130
- finish(e instanceof Error ? e : new Error(String(e)));
4131
- }
4132
- });
4133
- const timer = setTimeout(() => {
4134
- finish(new Error("Linear sign-in timed out \u2014 try again"));
4135
- }, timeoutMs);
4136
- const onAbort = () => finish(new LinearOAuthCancelledError());
4137
- const cleanup = () => {
4138
- clearTimeout(timer);
4139
- opts?.signal?.removeEventListener("abort", onAbort);
4140
- server.close();
4141
- };
4142
- opts?.signal?.addEventListener("abort", onAbort, { once: true });
4143
- if (opts?.signal?.aborted) {
4144
- onAbort();
4145
- return;
4146
- }
4147
- server.on("error", (e) => {
4148
- finish(
4149
- e instanceof Error && e.code === "EADDRINUSE" ? new Error(
4150
- `Port ${LINEAR_OAUTH_PORT} is in use. Close whatever is bound there, or paste a Linear API key instead.`
4151
- ) : e instanceof Error ? e : new Error(String(e))
4152
- );
4153
- });
4154
- server.listen(LINEAR_OAUTH_PORT, "127.0.0.1", () => {
4155
- void Promise.resolve(opts?.openUrl?.(authorizeUrl)).catch((e) => {
4156
- finish(e instanceof Error ? e : new Error(String(e)));
4157
- });
4158
- });
4159
- });
4160
- const body = new URLSearchParams({
4161
- grant_type: "authorization_code",
4162
- code,
4163
- redirect_uri: LINEAR_OAUTH_REDIRECT,
4164
- client_id: clientId,
4165
- code_verifier: pkce.verifier
4166
- });
4167
- if (clientSecret) body.set("client_secret", clientSecret);
4168
- const data = await exchangeLinearToken(body);
4169
- const viewer = await fetchLinearViewer(data.access_token.trim()).catch(() => ({}));
4170
- return persistLinearTokens(data, viewer);
4171
- }
4172
- async function revokeLinearToken(token, tokenTypeHint) {
4173
- const body = new URLSearchParams({
4174
- token,
4175
- token_type_hint: tokenTypeHint
4176
- });
4177
- await httpFetch(LINEAR_REVOKE, {
4178
- method: "POST",
4179
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
4180
- body
4181
- }).catch(() => void 0);
4182
- }
4183
- async function disconnectLinear() {
4184
- const settings = loadAppSettings();
4185
- const access = settings.integrations.linearAccessToken?.trim();
4186
- const refresh = settings.integrations.linearRefreshToken?.trim();
4187
- if (access) await revokeLinearToken(access, "access_token");
4188
- if (refresh) await revokeLinearToken(refresh, "refresh_token");
4189
- return disconnectLinearConnection();
4190
- }
4191
-
4192
- // src/integrations/linear-attachments.ts
4193
- var LINEAR_GRAPHQL2 = "https://api.linear.app/graphql";
4194
- var LINEAR_FILE_HOSTS = ["linear.app", "uploads.linear.app", "linearusercontent.com"];
4195
- var LINEAR_LINK_SOURCE_TYPES = /* @__PURE__ */ new Set([
4196
- "github",
4197
- "githubPullRequest",
4198
- "gitlab",
4199
- "figma",
4200
- "slack",
4201
- "sentry",
4202
- "zendesk",
4203
- "intercom",
4204
- "front"
4205
- ]);
4206
- var ATTACHMENT_QUERY = `
4207
- query SideboardAttachment($id: String!) {
4208
- attachment(id: $id) {
4209
- id
4210
- title
4211
- url
4212
- subtitle
4213
- sourceType
4214
- }
4215
- }
4216
- `;
4217
- function mapLinearAttachments(node) {
4218
- const listed = [];
4219
- for (const attachment of node.attachments?.nodes ?? []) {
4220
- const id = String(attachment.id ?? "");
4221
- const url = String(attachment.url ?? "").trim();
4222
- if (!id && !url) continue;
4223
- listed.push({
4224
- id,
4225
- title: String(attachment.title ?? ""),
4226
- url,
4227
- subtitle: attachment.subtitle?.trim() || void 0,
4228
- sourceType: attachment.sourceType?.trim() || void 0
4229
- });
4230
- }
4231
- const embedded = [
4232
- ...extractHttpUrls(node.description).filter((url) => hostMatches(url, LINEAR_FILE_HOSTS)),
4233
- ...(node.comments?.nodes ?? []).flatMap(
4234
- (comment) => extractHttpUrls(comment.body).filter((url) => hostMatches(url, LINEAR_FILE_HOSTS))
4235
- )
4236
- ];
4237
- const merged = mergeIssueAttachments([
4238
- ...listed.map((item) => ({
4239
- id: item.id,
4240
- name: item.title || item.subtitle || "attachment",
4241
- url: item.url,
4242
- sourceType: item.sourceType
4243
- })),
4244
- ...embedded.map((url) => ({
4245
- id: url,
4246
- name: url.split("/").pop() || "image",
4247
- url,
4248
- sourceType: "upload"
4249
- }))
4250
- ]);
4251
- const byUrl = new Map(listed.map((item) => [item.url, item]));
4252
- return merged.map((item) => {
4253
- const prior = byUrl.get(item.url);
4254
- return {
4255
- id: prior?.id || item.id,
4256
- title: prior?.title || item.name,
4257
- url: item.url,
4258
- subtitle: prior?.subtitle,
4259
- sourceType: prior?.sourceType || item.sourceType
4260
- };
4261
- });
4262
- }
4263
- function assertLinearFileUrl(url, sourceType) {
4264
- const kind = sourceType?.trim();
4265
- if (kind && LINEAR_LINK_SOURCE_TYPES.has(kind)) {
4266
- throw new Error(
4267
- `Linear attachment is a ${kind} link, not a file. Open the URL in the browser instead of downloading it.`
4268
- );
4269
- }
4270
- if (!hostMatches(url, LINEAR_FILE_HOSTS)) {
4271
- throw new Error(
4272
- `Linear attachment URL is not a Linear upload (${url}). Only uploads.linear.app / linear.app file hosts can be downloaded with the Linear token.`
4273
- );
4274
- }
4275
- }
4276
- async function requireLinearToken(apiKey) {
4277
- const token = (apiKey ?? await getLinearAuthToken())?.trim();
4278
- if (!token) {
4279
- throw new Error("Linear is not connected \u2014 sign in from Account settings");
4280
- }
4281
- return token;
4282
- }
4283
- async function downloadLinearAttachment(input, opts) {
4284
- const id = input.id.trim();
4285
- if (!id) throw new Error("Linear attachment id or URL is required");
4286
- const apiKey = await requireLinearToken(opts?.apiKey);
4287
- let url = id;
4288
- let name = input.name?.trim();
4289
- let sourceType;
4290
- if (!looksLikeHttpUrl(id)) {
4291
- const res = await httpFetch(LINEAR_GRAPHQL2, {
4292
- method: "POST",
4293
- headers: {
4294
- "Content-Type": "application/json",
4295
- Authorization: linearAuthorizationHeader(apiKey)
4296
- },
4297
- body: JSON.stringify({ query: ATTACHMENT_QUERY, variables: { id } })
4298
- });
4299
- if (!res.ok) {
4300
- throw new Error(`Linear API error ${res.status} looking up attachment ${id}`);
4301
- }
4302
- const json = await res.json();
4303
- if (json.errors?.length) {
4304
- throw new Error(json.errors.map((e) => e.message ?? "Linear error").join("; "));
4305
- }
4306
- const attachment = json.data?.attachment;
4307
- if (!attachment?.url?.trim()) {
4308
- throw new Error(`Linear attachment not found: ${id}`);
4309
- }
4310
- url = attachment.url.trim();
4311
- name = name || attachment.title?.trim() || attachment.subtitle?.trim() || void 0;
4312
- sourceType = attachment.sourceType?.trim() || void 0;
4313
- }
4314
- assertLinearFileUrl(url, sourceType);
4315
- return downloadAuthenticatedFile({
4316
- url,
4317
- headers: { Authorization: linearAuthorizationHeader(apiKey) },
4318
- worktreePath: issueAttachmentWorktreePath(opts?.destPath),
4319
- name
4320
- });
4321
- }
4322
-
4323
- // src/integrations/linear.ts
4324
- var LINEAR_GRAPHQL3 = "https://api.linear.app/graphql";
4325
- var LIST_ISSUE_FIELDS = `
4326
- id
4327
- identifier
4328
- title
4329
- url
4330
- createdAt
4331
- updatedAt
4332
- assignee { id name }
4333
- team { id key }
4334
- labels(first: 10) { nodes { name } }
4335
- cycle { id name number startsAt endsAt completedAt }
4336
- project { id name }
4337
- `;
4338
- var ISSUE_REF_FIELDS = `
4339
- id
4340
- identifier
4341
- title
4342
- url
4343
- `;
4344
- var ISSUE_FIELDS = `
4345
- id
4346
- identifier
4347
- title
4348
- description
4349
- url
4350
- priority
4351
- estimate
4352
- dueDate
4353
- branchName
4354
- createdAt
4355
- updatedAt
4356
- completedAt
4357
- state { id name type }
4358
- assignee { id name }
4359
- creator { id name }
4360
- team { id key name }
4361
- labels(first: 50) { nodes { name } }
4362
- cycle { id name number startsAt endsAt completedAt }
4363
- project { id name }
4364
- parent { ${ISSUE_REF_FIELDS} }
4365
- children(first: 25) { nodes { ${ISSUE_REF_FIELDS} } }
4366
- relations(first: 25) {
4367
- nodes {
4368
- id
4369
- type
4370
- relatedIssue { ${ISSUE_REF_FIELDS} }
4371
- }
4372
- }
4373
- inverseRelations(first: 25) {
4374
- nodes {
4375
- id
4376
- type
4377
- issue { ${ISSUE_REF_FIELDS} }
4378
- }
4379
- }
4380
- comments(first: 50) {
4381
- nodes {
4382
- id
4383
- body
4384
- url
4385
- createdAt
4386
- updatedAt
4387
- user { id name }
4388
- }
4389
- }
4390
- attachments(first: 20) {
4391
- nodes { id title url subtitle sourceType }
4392
- }
4393
- `;
4394
- var ASSIGNED_ISSUES_QUERY = `
4395
- query SideboardAssignedIssues($first: Int!, $filter: IssueFilter) {
4396
- viewer {
4397
- id
4398
- name
4399
- assignedIssues(
4400
- first: $first
4401
- orderBy: updatedAt
4402
- filter: $filter
4403
- ) {
4404
- nodes { ${LIST_ISSUE_FIELDS} }
4405
- }
4406
- }
4407
- }
4408
- `;
4409
- var COMMENTS_SINCE_QUERY = `
4410
- query SideboardCommentsSince($first: Int!, $filter: CommentFilter) {
4411
- viewer { id name }
4412
- comments(first: $first, orderBy: createdAt, filter: $filter) {
4413
- nodes {
4414
- id
4415
- body
4416
- url
4417
- createdAt
4418
- user { id name }
4419
- issue { identifier title url }
4420
- }
4421
- }
4422
- }
4423
- `;
4424
- var ISSUES_QUERY = `
4425
- query SideboardIssues($first: Int!, $filter: IssueFilter) {
4426
- viewer { id name }
4427
- issues(first: $first, orderBy: updatedAt, filter: $filter) {
4428
- nodes { ${LIST_ISSUE_FIELDS} }
4429
- }
4430
- }
4431
- `;
4432
- var SEARCH_ISSUES_QUERY = `
4433
- query SideboardSearchIssues($term: String!, $first: Int!, $filter: IssueFilter) {
4434
- viewer { id name }
4435
- searchIssues(term: $term, first: $first, includeArchived: false, filter: $filter) {
4436
- nodes { ${LIST_ISSUE_FIELDS} }
4437
- }
4438
- }
4439
- `;
4440
- var TEAMS_QUERY = `
4441
- query SideboardTeams {
4442
- viewer { id name }
4443
- teams(first: 50) {
4444
- nodes {
4445
- id
4446
- key
4447
- name
4448
- states(first: 50) { nodes { id name type } }
4449
- activeCycle { id name number startsAt endsAt completedAt }
4450
- }
4451
- }
4452
- }
4453
- `;
4454
- var TEAM_CYCLES_QUERY = `
4455
- query SideboardTeamCycles($id: String!) {
4456
- team(id: $id) {
4457
- id
4458
- key
4459
- activeCycle { id name number startsAt endsAt completedAt }
4460
- cycles(first: 40) {
4461
- nodes { id name number startsAt endsAt completedAt }
4462
- }
4463
- }
4464
- }
4465
- `;
4466
- var ISSUE_QUERY = `
4467
- query SideboardIssue($id: String!) {
4468
- issue(id: $id) { ${ISSUE_FIELDS} }
4469
- }
4470
- `;
4471
- var ISSUE_CREATE = `
4472
- mutation SideboardIssueCreate($input: IssueCreateInput!) {
4473
- issueCreate(input: $input) {
4474
- success
4475
- issue { ${ISSUE_FIELDS} }
4476
- }
4477
- }
4478
- `;
4479
- var ISSUE_UPDATE = `
4480
- mutation SideboardIssueUpdate($id: String!, $input: IssueUpdateInput!) {
4481
- issueUpdate(id: $id, input: $input) {
4482
- success
4483
- issue { ${ISSUE_FIELDS} }
4484
- }
4485
- }
4486
- `;
4487
- var COMMENT_CREATE = `
4488
- mutation SideboardCommentCreate($input: CommentCreateInput!) {
4489
- commentCreate(input: $input) {
4490
- success
4491
- comment { id body url }
4492
- }
4493
- }
4494
- `;
4495
- var LABELS_QUERY = `
4496
- query SideboardLabels($first: Int!) {
4497
- issueLabels(first: $first) {
4498
- nodes { id name }
4499
- }
4500
- }
4501
- `;
4502
- var TEAM_LABELS_QUERY = `
4503
- query SideboardTeamLabels($id: String!, $first: Int!, $after: String) {
4504
- team(id: $id) {
4505
- labels(first: $first, after: $after) {
4506
- nodes { id name }
4507
- pageInfo { hasNextPage endCursor }
4508
- }
4509
- }
4510
- }
4511
- `;
4512
- var PROJECTS_QUERY = `
4513
- query SideboardProjects($first: Int!, $filter: ProjectFilter, $after: String) {
4514
- projects(first: $first, filter: $filter, after: $after) {
4515
- nodes { id name slugId }
4516
- pageInfo { hasNextPage endCursor }
4517
- }
4518
- }
4519
- `;
4520
- var ISSUE_RELATION_CREATE = `
4521
- mutation SideboardIssueRelationCreate($input: IssueRelationCreateInput!) {
4522
- issueRelationCreate(input: $input) {
4523
- success
4524
- issueRelation { id type }
4525
- }
4526
- }
4527
- `;
4528
- var ISSUE_RELATION_DELETE = `
4529
- mutation SideboardIssueRelationDelete($id: String!) {
4530
- issueRelationDelete(id: $id) {
4531
- success
4532
- }
4533
- }
4534
- `;
4535
- function rewriteLinearError(message) {
4536
- if (/scope|permission|not (authorized|allowed)|unauthorized|insufficient/i.test(message)) {
4537
- return `${message} \u2014 Disconnect and Connect Linear in Account settings to grant write access.`;
4538
- }
4539
- return message;
4540
- }
4541
- async function requireLinearToken2(apiKey) {
4542
- const token = (apiKey ?? await getLinearAuthToken())?.trim();
4543
- if (!token) {
4544
- throw new Error("Linear is not connected \u2014 sign in from Account settings");
4545
- }
4546
- return token;
4547
- }
4548
- async function linearGraphql(query, variables, opts) {
4549
- const apiKey = await requireLinearToken2(opts?.apiKey);
4550
- const res = await httpFetch(LINEAR_GRAPHQL3, {
4551
- method: "POST",
4552
- headers: {
4553
- "Content-Type": "application/json",
4554
- Authorization: linearAuthorizationHeader(apiKey)
4555
- },
4556
- body: JSON.stringify({ query, variables })
4557
- });
4558
- if (!res.ok) {
4559
- const body = await res.text().catch(() => "");
4560
- throw new Error(
4561
- rewriteLinearError(
4562
- `Linear API error ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`
4563
- )
4564
- );
4565
- }
4566
- const json = await res.json();
4567
- if (json.errors?.length) {
4568
- throw new Error(
4569
- rewriteLinearError(json.errors.map((e) => e.message ?? "Linear error").join("; "))
4570
- );
4571
- }
4572
- if (json.data == null) {
4573
- throw new Error("Linear API returned no data");
4574
- }
4575
- return json.data;
4576
- }
4577
- function mapState(node) {
4578
- if (!node?.id) return void 0;
4579
- return {
4580
- id: String(node.id),
4581
- name: String(node.name ?? ""),
4582
- type: String(node.type ?? "")
4583
- };
4584
- }
4585
- function linearCycleIsActive(cycle, now = Date.now()) {
4586
- if (!cycle) return false;
4587
- if (cycle.completedAt) return false;
4588
- const start = cycle.startsAt ? Date.parse(cycle.startsAt) : Number.NaN;
4589
- const end = cycle.endsAt ? Date.parse(cycle.endsAt) : Number.NaN;
4590
- if (Number.isFinite(start) && now < start) return false;
4591
- if (Number.isFinite(end) && now > end) return false;
4592
- return true;
4593
- }
4594
- function cycleDisplayName(node) {
4595
- return String(node?.name ?? (node?.number != null ? `Cycle ${node.number}` : "")).trim();
4596
- }
4597
- function mapNamedCycle(node) {
4598
- if (!node?.id) return null;
4599
- const name = cycleDisplayName(node);
4600
- if (!name) return null;
4601
- return {
4602
- id: String(node.id),
4603
- name,
4604
- number: typeof node.number === "number" ? node.number : void 0,
4605
- isActive: linearCycleIsActive(node)
4606
- };
4607
- }
4608
- function mapCycle(node) {
4609
- if (!node?.name && node?.number == null) return null;
4610
- const name = cycleDisplayName(node);
4611
- if (!name) return null;
4612
- return {
4613
- ...node.id ? { id: String(node.id) } : {},
4614
- name,
4615
- number: typeof node.number === "number" ? node.number : void 0,
4616
- isActive: linearCycleIsActive(node)
4617
- };
4618
- }
4619
- function mapUser(node) {
4620
- if (!node?.id && !node?.name) return void 0;
4621
- return {
4622
- id: String(node.id ?? ""),
4623
- name: String(node.name ?? "")
4624
- };
4625
- }
4626
- function mapIssueRef(node) {
4627
- if (!node?.id && !node?.identifier) return void 0;
4628
- return {
4629
- id: String(node.id ?? node.identifier ?? ""),
4630
- identifier: String(node.identifier ?? node.id ?? ""),
4631
- title: String(node.title ?? ""),
4632
- url: String(node.url ?? "")
4633
- };
4634
- }
4635
- function flipLinearRelationType(type) {
4636
- switch (type) {
4637
- case "blocks":
4638
- return "blockedBy";
4639
- case "blockedBy":
4640
- return "blocks";
4641
- case "duplicate":
4642
- return "duplicateOf";
4643
- case "duplicateOf":
4644
- return "duplicate";
4645
- default:
4646
- return type;
4647
- }
4648
- }
4649
- function mapRelations(node) {
4650
- const out = [];
4651
- for (const rel of node.relations?.nodes ?? []) {
4652
- const issue = mapIssueRef(rel.relatedIssue);
4653
- if (!issue) continue;
4654
- const id = rel.id?.trim();
4655
- out.push({ type: String(rel.type ?? "related"), issue, ...id ? { id } : {} });
4656
- }
4657
- for (const rel of node.inverseRelations?.nodes ?? []) {
4658
- const issue = mapIssueRef(rel.issue);
4659
- if (!issue) continue;
4660
- const id = rel.id?.trim();
4661
- out.push({
4662
- type: flipLinearRelationType(String(rel.type ?? "related")),
4663
- issue,
4664
- ...id ? { id } : {}
4665
- });
4666
- }
4667
- return out;
4668
- }
4669
- function mapComments2(node) {
4670
- const out = [];
4671
- for (const comment of node.comments?.nodes ?? []) {
4672
- const id = String(comment.id ?? "");
4673
- const body = String(comment.body ?? "");
4674
- if (!id && !body) continue;
4675
- out.push({
4676
- id,
4677
- body,
4678
- url: comment.url?.trim() || void 0,
4679
- createdAt: comment.createdAt?.trim() || void 0,
4680
- updatedAt: comment.updatedAt?.trim() || void 0,
4681
- user: mapUser(comment.user)
4682
- });
4683
- }
4684
- return out;
4685
- }
4686
- function mapIssue(node) {
4687
- const team = node.team;
4688
- return {
4689
- id: String(node.id ?? node.identifier ?? ""),
4690
- identifier: String(node.identifier ?? node.id ?? ""),
4691
- title: String(node.title ?? ""),
4692
- url: String(node.url ?? ""),
4693
- description: node.description?.trim() || void 0,
4694
- priority: typeof node.priority === "number" ? node.priority : void 0,
4695
- estimate: typeof node.estimate === "number" ? node.estimate : void 0,
4696
- dueDate: node.dueDate?.trim() || void 0,
4697
- branchName: node.branchName?.trim() || void 0,
4698
- createdAt: node.createdAt?.trim() || void 0,
4699
- updatedAt: node.updatedAt?.trim() || void 0,
4700
- completedAt: node.completedAt?.trim() || void 0,
4701
- state: mapState(node.state),
4702
- assignee: mapUser(node.assignee),
4703
- creator: mapUser(node.creator),
4704
- team: team?.id ? {
4705
- id: String(team.id),
4706
- key: String(team.key ?? ""),
4707
- name: String(team.name ?? ""),
4708
- states: (team.states?.nodes ?? []).flatMap((s) => {
4709
- const state = mapState(s);
4710
- return state ? [state] : [];
4711
- })
4712
- } : void 0,
4713
- labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n)),
4714
- cycle: mapCycle(node.cycle),
4715
- project: node.project?.id ? { id: String(node.project.id), name: String(node.project.name ?? "") } : void 0,
4716
- parent: mapIssueRef(node.parent),
4717
- children: (node.children?.nodes ?? []).flatMap((child) => {
4718
- const ref = mapIssueRef(child);
4719
- return ref ? [ref] : [];
4720
- }),
4721
- relations: mapRelations(node),
4722
- comments: mapComments2(node),
4723
- attachments: mapLinearAttachments(node)
4724
- };
4725
- }
4726
- function toIssueInfo(issue) {
4727
- return {
4728
- id: issue.id,
4729
- identifier: issue.identifier,
4730
- title: issue.title,
4731
- url: issue.url,
4732
- labels: issue.labels,
4733
- provider: "linear",
4734
- assignee: issue.assignee?.name,
4735
- assignees: issue.assignee?.name ? [issue.assignee.name] : void 0,
4736
- cycle: issue.cycle ?? null,
4737
- teamKey: issue.team?.key || void 0,
4738
- ...issue.project ? { projects: [issue.project] } : {},
4739
- ...issue.createdAt ? { createdAt: issue.createdAt } : {},
4740
- ...issue.updatedAt ? { updatedAt: issue.updatedAt } : {}
4741
- };
4742
- }
4743
- function mapTeam(node) {
4744
- const cycles = (node.cycles?.nodes ?? []).flatMap((c) => {
4745
- const cycle = mapNamedCycle(c);
4746
- return cycle ? [cycle] : [];
4747
- });
4748
- return {
4749
- id: String(node.id ?? ""),
4750
- key: String(node.key ?? ""),
4751
- name: String(node.name ?? ""),
4752
- states: (node.states?.nodes ?? []).flatMap((s) => {
4753
- const state = mapState(s);
4754
- return state ? [state] : [];
4755
- }),
4756
- activeCycle: mapNamedCycle(node.activeCycle),
4757
- ...cycles.length ? { cycles } : {}
4758
- };
4759
- }
4760
- function resolveLinearTeam(teams, team) {
4761
- const t = team.trim();
4762
- if (!t) throw new Error("Linear team is required (id, key, or name from linear_list_teams)");
4763
- const lower = t.toLowerCase();
4764
- const found = teams.find(
4765
- (x) => x.id === t || x.key.toLowerCase() === lower || x.name.toLowerCase() === lower
4766
- );
4767
- if (!found) {
4768
- const keys = teams.map((x) => x.key).filter(Boolean).join(", ") || "(none)";
4769
- throw new Error(`Linear team not found: ${team}. Known keys: ${keys}`);
4770
- }
4771
- return found;
4772
- }
4773
- function resolveLinearState(team, state) {
4774
- const s = state.trim();
4775
- if (!s) throw new Error("Linear state is empty");
4776
- const lower = s.toLowerCase();
4777
- const found = team.states.find((x) => x.id === s) || team.states.find((x) => x.name.toLowerCase() === lower) || team.states.find((x) => x.type.toLowerCase() === lower);
4778
- if (!found) {
4779
- const available = team.states.map((x) => `${x.name} (${x.type})`).join(", ") || "(none)";
4780
- throw new Error(`Linear state not found on ${team.key}: ${state}. Available: ${available}`);
4781
- }
4782
- return found;
4783
- }
4784
- function resolveLinearCycle(team, cycle) {
4785
- if (cycle == null) return null;
4786
- const s = cycle.trim();
4787
- if (!s || /^(none|clear|unschedule|null)$/i.test(s)) return null;
4788
- if (/^(current|active|this)$/i.test(s)) {
4789
- const active = team.activeCycle ?? team.cycles?.find((c) => c.isActive);
4790
- if (!active?.id) {
4791
- throw new Error(`Linear team ${team.key} has no active cycle`);
4792
- }
4793
- return active.id;
4794
- }
4795
- const lower = s.toLowerCase();
4796
- const asNumber = Number.parseInt(s, 10);
4797
- const seen = /* @__PURE__ */ new Set();
4798
- const cycles = [team.activeCycle, ...team.cycles ?? []].filter(
4799
- (c) => {
4800
- if (!c?.id || seen.has(c.id)) return false;
4801
- seen.add(c.id);
4802
- return true;
4803
- }
4804
- );
4805
- const found = cycles.find((c) => c.id === s) || cycles.find((c) => c.name.toLowerCase() === lower) || (Number.isInteger(asNumber) ? cycles.find((c) => c.number === asNumber) : void 0);
4806
- if (!found) {
4807
- const available = cycles.map((c) => c.number != null ? `${c.name} (${c.number})` : c.name).join(", ") || "(none)";
4808
- throw new Error(`Linear cycle not found on ${team.key}: ${cycle}. Available: ${available}`);
4809
- }
4810
- return found.id;
4811
- }
4812
- var LINEAR_NONE_RE = /^(none|clear|unschedule|null)$/i;
4813
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4814
- function isLinearNoneToken(value) {
4815
- if (value == null) return true;
4816
- const s = value.trim();
4817
- return !s || LINEAR_NONE_RE.test(s);
4818
- }
4819
- function resolveLinearLabel(labels, name) {
4820
- const s = name.trim();
4821
- if (!s) throw new Error("Linear label is empty");
4822
- const lower = s.toLowerCase();
4823
- const found = labels.find((x) => x.id === s) || labels.find((x) => x.name.toLowerCase() === lower);
4824
- if (!found) {
4825
- const available = labels.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
4826
- throw new Error(`Linear label not found: ${name}. Available: ${available}`);
4827
- }
4828
- return found;
4829
- }
4830
- function resolveLinearProject(projects, project) {
4831
- if (isLinearNoneToken(project)) return null;
4832
- const s = (project ?? "").trim();
4833
- const lower = s.toLowerCase();
4834
- const found = projects.find((x) => x.id === s) || projects.find((x) => x.name.toLowerCase() === lower) || projects.find((x) => x.slugId?.toLowerCase() === lower);
4835
- if (!found) {
4836
- const available = projects.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
4837
- throw new Error(`Linear project not found: ${project}. Available: ${available}`);
4838
- }
4839
- return found.id;
4840
- }
4841
- function normalizeLinearRelationType(type) {
4842
- const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
4843
- if (s === "blocks" || s === "block") return "blocks";
4844
- if (s === "blockedby" || s === "blocked") return "blockedBy";
4845
- if (s === "related" || s === "relates" || s === "relatesto") return "related";
4846
- if (s === "duplicate" || s === "dup") return "duplicate";
4847
- if (s === "duplicateof" || s === "original") return "duplicateOf";
4848
- if (s === "similar") return "similar";
4849
- throw new Error(
4850
- `Linear relation type must be blocks, blockedBy, related, duplicate, or duplicateOf (got ${type})`
4851
- );
4852
- }
4853
- function linearRelationCreateInput(thisIssueId, type, relatedIssueId) {
4854
- const view = normalizeLinearRelationType(type);
4855
- if (view === "blockedBy") {
4856
- return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "blocks" };
4857
- }
4858
- if (view === "duplicateOf") {
4859
- return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "duplicate" };
4860
- }
4861
- return { issueId: thisIssueId, relatedIssueId, type: view };
4862
- }
4863
- function relationMatches(rel, type, issueRef) {
4864
- if (rel.type.toLowerCase() !== type.toLowerCase()) return false;
4865
- const ref = issueRef.trim().toLowerCase();
4866
- return rel.issue.id.toLowerCase() === ref || rel.issue.identifier.toLowerCase() === ref;
4867
- }
4868
- function formatRelationList(relations) {
4869
- if (!relations.length) return "(none)";
4870
- return relations.map((r) => `${r.type} ${r.issue.identifier || r.issue.id}`).join(", ");
4871
- }
4872
- function mapLabelNodes(nodes) {
4873
- return (nodes ?? []).flatMap((node) => {
4874
- const id = String(node.id ?? "").trim();
4875
- const name = String(node.name ?? "").trim();
4876
- if (!id) return [];
4877
- return [{ id, name }];
4878
- });
4879
- }
4880
- async function listLinearWorkspaceLabels(opts) {
4881
- const json = await linearGraphql(LABELS_QUERY, { first: 250 }, opts);
4882
- return mapLabelNodes(json.issueLabels?.nodes);
4883
- }
4884
- async function listLinearLabels(opts) {
4885
- const teamId = opts?.teamId?.trim();
4886
- if (teamId) {
4887
- const out = [];
4888
- let after;
4889
- for (let page = 0; page < 4; page++) {
4890
- const json = await linearGraphql(
4891
- TEAM_LABELS_QUERY,
4892
- after ? { id: teamId, first: 250, after } : { id: teamId, first: 250 },
4893
- opts
4894
- );
4895
- const conn = json.team?.labels;
4896
- out.push(...mapLabelNodes(conn?.nodes));
4897
- if (!conn?.pageInfo?.hasNextPage || !conn.pageInfo.endCursor) break;
4898
- after = conn.pageInfo.endCursor;
4899
- }
4900
- if (out.length) return out;
4901
- }
4902
- return listLinearWorkspaceLabels(opts);
4903
- }
4904
- async function queryLinearProjects(filter, opts) {
4905
- const out = [];
4906
- let after;
4907
- for (let page = 0; page < 4; page++) {
4908
- const json = await linearGraphql(PROJECTS_QUERY, { first: 50, filter: filter ?? {}, ...after ? { after } : {} }, opts);
4909
- for (const node of json.projects?.nodes ?? []) {
4910
- const id = String(node.id ?? "").trim();
4911
- if (!id) continue;
4912
- out.push({ id, name: String(node.name ?? ""), ...node.slugId ? { slugId: String(node.slugId) } : {} });
4913
- }
4914
- const pageInfo = json.projects?.pageInfo;
4915
- if (!pageInfo?.hasNextPage || !pageInfo.endCursor) break;
4916
- after = pageInfo.endCursor;
4917
- }
4918
- return out;
4919
- }
4920
- function listLinearProjects(opts) {
4921
- return queryLinearProjects(void 0, opts);
4922
- }
4923
- async function resolveLinearProjectId(project, opts) {
4924
- if (isLinearNoneToken(project)) return null;
4925
- const s = (project ?? "").trim();
4926
- const filter = UUID_RE.test(s) ? { id: { eq: s } } : { name: { eqIgnoreCase: s } };
4927
- const matched = await queryLinearProjects(filter, opts);
4928
- try {
4929
- return resolveLinearProject(matched, s);
4930
- } catch {
4931
- if (UUID_RE.test(s)) return s;
4932
- const listed = await queryLinearProjects(void 0, opts);
4933
- return resolveLinearProject(listed, s);
4934
- }
4935
- }
4936
- async function resolveLinearLabelIds(names, opts) {
4937
- if (names.length === 0) return [];
4938
- const labels = await listLinearLabels(opts);
4939
- return names.map((name) => {
4940
- const s = name.trim();
4941
- try {
4942
- return resolveLinearLabel(labels, s).id;
4943
- } catch (error) {
4944
- if (UUID_RE.test(s)) return s;
4945
- throw error;
4946
- }
4947
- });
4948
- }
4949
- async function applyLinearIssueRelations(issue, add, remove, opts) {
4950
- const current = [...issue.relations];
4951
- for (const rel of remove ?? []) {
4952
- const type = normalizeLinearRelationType(rel.type);
4953
- const issueRef = rel.issue.trim();
4954
- if (!issueRef) throw new Error("Linear removeRelations.issue is required (uuid or ENG-123)");
4955
- const match = current.find((r) => relationMatches(r, type, issueRef));
4956
- if (!match?.id) {
4957
- throw new Error(
4958
- `Linear relation not found: ${type} ${issueRef}. Existing: ${formatRelationList(current)}`
4959
- );
4960
- }
4961
- const json = await linearGraphql(ISSUE_RELATION_DELETE, { id: match.id }, opts);
4962
- if (!json.issueRelationDelete?.success) {
4963
- throw new Error(`Linear issueRelationDelete failed (${type} ${issueRef})`);
4964
- }
4965
- const idx = current.indexOf(match);
4966
- if (idx >= 0) current.splice(idx, 1);
4967
- }
4968
- for (const rel of add ?? []) {
4969
- const type = normalizeLinearRelationType(rel.type);
4970
- const issueRef = rel.issue.trim();
4971
- if (!issueRef) throw new Error("Linear relations.issue is required (uuid or ENG-123)");
4972
- if (current.some((r) => relationMatches(r, type, issueRef))) continue;
4973
- const related = await getLinearIssue(issueRef, opts);
4974
- const input = linearRelationCreateInput(issue.id, type, related.id);
4975
- const json = await linearGraphql(ISSUE_RELATION_CREATE, { input }, opts);
4976
- if (!json.issueRelationCreate?.success) {
4977
- throw new Error(`Linear issueRelationCreate failed (${type} ${related.identifier})`);
4978
- }
4979
- current.push({ type, issue: related, id: void 0 });
4980
- }
4981
- }
4982
- function normalizePriority(priority) {
4983
- if (priority == null) return void 0;
4984
- if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
4985
- throw new Error("Linear priority must be 0 (none), 1 (urgent), 2 (high), 3 (medium), or 4 (low)");
4986
- }
4987
- return priority;
4988
- }
4989
- function assigneeFilterKey(assignee) {
4990
- return (assignee ?? "").trim();
4991
- }
4992
- function applyLinearIssueQueryFilter(filter, query) {
4993
- const term = query?.trim();
4994
- if (!term) return;
4995
- const or = [
4996
- { title: { containsIgnoreCase: term } },
4997
- { description: { containsIgnoreCase: term } }
4998
- ];
4999
- const identifierMatch = term.match(/^[A-Za-z][\w]*-(\d+)$/);
5000
- const asNumber = identifierMatch ? Number(identifierMatch[1]) : /^\d+$/.test(term) ? Number(term) : NaN;
5001
- if (Number.isFinite(asNumber) && asNumber > 0) {
5002
- or.push({ number: { eq: asNumber } });
5003
- }
5004
- filter.or = or;
5005
- }
5006
- function buildLinearIssueFilter(input) {
5007
- const filter = {
5008
- state: { type: { nin: ["completed", "canceled"] } }
5009
- };
5010
- const since = input?.updatedSince?.trim();
5011
- if (since) filter.updatedAt = { gte: since };
5012
- applyLinearIssueQueryFilter(filter, input?.query);
5013
- const raw = assigneeFilterKey(input?.assignee) || "me";
5014
- const key = raw.toLowerCase();
5015
- if (key === "all" || key === "*") return filter;
5016
- if (key === "unassigned" || key === "none" || key === "null") {
5017
- filter.assignee = { null: true };
5018
- return filter;
5019
- }
5020
- if (key === "me" || key === "@me") {
5021
- filter.assignee = { isMe: { eq: true } };
5022
- return filter;
5023
- }
5024
- if (UUID_RE.test(raw)) {
5025
- filter.assignee = { id: { eq: raw } };
5026
- return filter;
5027
- }
5028
- filter.assignee = { name: { eqIgnoreCase: raw } };
5029
- return filter;
5030
- }
5031
- function isAssignedToMe(assignee) {
5032
- const key = assignee.toLowerCase();
5033
- return key === "me" || key === "@me";
3741
+ return Number.isFinite(number) && number > 0 ? number : null;
5034
3742
  }
5035
- function listedLinearIssues(json, nodes) {
3743
+ var GITHUB_ISSUE_COMMENTS_PAGE_SIZE = 100;
3744
+ var GITHUB_ISSUE_COMMENTS_MAX_PAGES = 5;
3745
+ function parseGitHubIssueCommentNode(item, allowed) {
3746
+ const rec = item && typeof item === "object" ? item : null;
3747
+ if (!rec) return null;
3748
+ const issueUrl = typeof rec.issue_url === "string" ? rec.issue_url : "";
3749
+ const number = issueNumberFromApiUrl(issueUrl);
3750
+ if (number == null) return null;
3751
+ if (allowed && !allowed.has(number)) return null;
3752
+ const body = previewIssueCommentBody(typeof rec.body === "string" ? rec.body : "");
3753
+ const user = rec.user && typeof rec.user === "object" ? String(rec.user.login ?? "").trim() : "";
3754
+ const createdAt = typeof rec.created_at === "string" ? rec.created_at : typeof rec.createdAt === "string" ? rec.createdAt : void 0;
3755
+ const url = typeof rec.html_url === "string" ? rec.html_url : void 0;
5036
3756
  return {
5037
- viewer: {
5038
- id: String(json.viewer?.id ?? ""),
5039
- name: String(json.viewer?.name ?? "")
5040
- },
5041
- issues: (nodes ?? []).map((node) => toIssueInfo(mapIssue(node)))
3757
+ identifier: `#${number}`,
3758
+ ...user ? { author: user } : {},
3759
+ ...createdAt ? { createdAt } : {},
3760
+ body,
3761
+ ...url ? { url } : {}
5042
3762
  };
5043
3763
  }
5044
- async function listLinearIssuesFiltered(opts) {
5045
- const first = Math.max(1, Math.min(250, opts?.limit ?? 200));
5046
- const query = opts?.query?.trim() ?? "";
5047
- const assignee = assigneeFilterKey(opts?.assignee) || (query ? "all" : "me");
5048
- const filter = buildLinearIssueFilter({
5049
- assignee,
5050
- updatedSince: opts?.updatedSince
5051
- });
5052
- if (query) {
5053
- const json2 = await linearGraphql(
5054
- SEARCH_ISSUES_QUERY,
5055
- { term: query, first, filter },
5056
- opts
5057
- );
5058
- return listedLinearIssues(json2, json2.searchIssues?.nodes);
5059
- }
5060
- if (isAssignedToMe(assignee)) {
5061
- const json2 = await linearGraphql(
5062
- ASSIGNED_ISSUES_QUERY,
5063
- { first, filter },
5064
- opts
5065
- );
5066
- return listedLinearIssues(json2, json2.viewer?.assignedIssues?.nodes);
5067
- }
5068
- const json = await linearGraphql(
5069
- ISSUES_QUERY,
5070
- { first, filter },
5071
- opts
5072
- );
5073
- return listedLinearIssues(json, json.issues?.nodes);
5074
- }
5075
- async function listLinearCommentsSince(opts) {
5076
- const first = Math.max(1, Math.min(250, opts.limit ?? 200));
5077
- const query = opts.query?.trim() ?? "";
3764
+ async function listGitHubIssueCommentsSince(opts) {
3765
+ const limit = Math.max(1, Math.min(250, opts.limit ?? 200));
3766
+ const { cwd, slug } = await resolveGitHubIssueRepo(opts.repoPath);
3767
+ if (!slug) return [];
5078
3768
  const restrictToIssues = opts.issueIdentifiers != null;
5079
3769
  const allowed = new Set(
5080
- (opts.issueIdentifiers ?? []).map((id) => id.trim()).filter(Boolean)
5081
- );
5082
- if (restrictToIssues && allowed.size === 0) return [];
5083
- const assignee = assigneeFilterKey(opts.assignee) || (query ? "all" : "me");
5084
- const json = await linearGraphql(
5085
- COMMENTS_SINCE_QUERY,
5086
- {
5087
- first,
5088
- filter: {
5089
- createdAt: { gte: opts.since },
5090
- issue: buildLinearIssueFilter({
5091
- assignee,
5092
- // Search hits may match identifier/comments — do not also require
5093
- // title/description contains when an allowlist is already applied.
5094
- query: restrictToIssues ? void 0 : query || void 0
5095
- })
3770
+ (opts.issueIdentifiers ?? []).map((id) => {
3771
+ try {
3772
+ return parseGitHubIssueNumber(id);
3773
+ } catch {
3774
+ return null;
5096
3775
  }
5097
- },
5098
- opts
3776
+ }).filter((n) => n != null)
5099
3777
  );
3778
+ if (restrictToIssues && allowed.size === 0) return [];
3779
+ const pageSize = GITHUB_ISSUE_COMMENTS_PAGE_SIZE;
5100
3780
  const out = [];
5101
- for (const node of json.comments?.nodes ?? []) {
5102
- const identifier = String(node.issue?.identifier ?? "").trim();
5103
- if (restrictToIssues && !allowed.has(identifier)) continue;
5104
- const body = previewIssueCommentBody(String(node.body ?? ""));
5105
- if (!identifier && !body) continue;
5106
- out.push({
5107
- identifier: identifier || String(node.id ?? ""),
5108
- ...node.issue?.title ? { title: String(node.issue.title) } : {},
5109
- ...node.user?.name ? { author: String(node.user.name) } : {},
5110
- ...node.createdAt?.trim() ? { createdAt: node.createdAt.trim() } : {},
5111
- body,
5112
- ...node.url?.trim() ? { url: node.url.trim() } : {}
5113
- });
3781
+ for (let page = 1; page <= GITHUB_ISSUE_COMMENTS_MAX_PAGES; page++) {
3782
+ const path2 = `repos/${slug}/issues/comments?since=${encodeURIComponent(opts.since)}&sort=created&direction=desc&per_page=${pageSize}&page=${page}`;
3783
+ const result = await gh(["api", path2], cwd, { reject: false });
3784
+ if (result.exitCode !== 0 || !result.stdout.trim()) break;
3785
+ let parsed;
3786
+ try {
3787
+ parsed = JSON.parse(result.stdout);
3788
+ } catch {
3789
+ break;
3790
+ }
3791
+ if (!Array.isArray(parsed) || parsed.length === 0) break;
3792
+ for (const item of parsed) {
3793
+ const comment = parseGitHubIssueCommentNode(item, restrictToIssues ? allowed : null);
3794
+ if (!comment) continue;
3795
+ out.push(comment);
3796
+ if (out.length >= limit) return out;
3797
+ }
3798
+ if (parsed.length < pageSize) break;
5114
3799
  }
5115
3800
  return out;
5116
3801
  }
5117
- async function listLinearAssignedIssues(opts) {
5118
- return listLinearIssuesFiltered({ ...opts, assignee: "me" });
5119
- }
5120
- async function listLinearIssuesDirect(opts) {
5121
- return (await listLinearAssignedIssues(opts)).issues;
5122
- }
5123
- async function listLinearTeams(opts) {
5124
- const json = await linearGraphql(TEAMS_QUERY, void 0, opts);
5125
- return {
5126
- viewer: {
5127
- id: String(json.viewer?.id ?? ""),
5128
- name: String(json.viewer?.name ?? "")
5129
- },
5130
- teams: (json.teams?.nodes ?? []).map(mapTeam)
5131
- };
5132
- }
5133
- async function listLinearTeamCycles(teamId, opts) {
5134
- const json = await linearGraphql(TEAM_CYCLES_QUERY, { id: teamId }, opts);
5135
- if (!json.team?.id) {
5136
- throw new Error(`Linear team not found: ${teamId}`);
3802
+ async function getGitHubIssue(id, opts) {
3803
+ const number = parseGitHubIssueNumber(id);
3804
+ const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
3805
+ const stdout = requireGhOk(
3806
+ await gh(
3807
+ [
3808
+ "issue",
3809
+ "view",
3810
+ String(number),
3811
+ ...repoArgs,
3812
+ "--json",
3813
+ "number,title,body,url,state,labels,assignees,comments,author"
3814
+ ],
3815
+ cwd,
3816
+ { reject: false }
3817
+ ),
3818
+ `GitHub issue ${number}`
3819
+ );
3820
+ let parsed;
3821
+ try {
3822
+ parsed = JSON.parse(stdout);
3823
+ } catch {
3824
+ throw new Error(`GitHub issue ${number}: gh returned non-JSON`);
5137
3825
  }
5138
- const mapped = mapTeam(json.team);
5139
- return {
5140
- id: mapped.id,
5141
- key: mapped.key,
5142
- activeCycle: mapped.activeCycle,
5143
- cycles: mapped.cycles ?? []
5144
- };
3826
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3827
+ throw new Error(`GitHub issue not found: #${number}`);
3828
+ }
3829
+ const issue = toGitHubIssue(parsed);
3830
+ const projects = await readGitHubIssueProjects(number, cwd, repoArgs);
3831
+ issue.projects = projects.projects;
3832
+ if (projects.error) issue.projectsError = projects.error;
3833
+ return issue;
5145
3834
  }
5146
- async function getLinearIssue(id, opts) {
5147
- const issueId = id.trim();
5148
- if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
5149
- const json = await linearGraphql(
5150
- ISSUE_QUERY,
5151
- { id: issueId },
5152
- opts
3835
+ async function commentGitHubIssue(input, opts) {
3836
+ const number = parseGitHubIssueNumber(input.id);
3837
+ const body = input.body.trim();
3838
+ if (!body) throw new Error("GitHub comment body is required");
3839
+ const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
3840
+ const stdout = requireGhOk(
3841
+ await gh(
3842
+ ["issue", "comment", String(number), ...repoArgs, "--body", body],
3843
+ cwd,
3844
+ { reject: false }
3845
+ ),
3846
+ `GitHub comment on #${number}`
5153
3847
  );
5154
- if (!json.issue) {
5155
- throw new Error(`Linear issue not found: ${issueId}`);
5156
- }
5157
- return mapIssue(json.issue);
3848
+ const url = stdout.trim().split(/\s+/).find((part) => /^https?:\/\//i.test(part));
3849
+ return { body, url };
5158
3850
  }
5159
- async function createLinearIssue(input, opts) {
5160
- const title = input.title.trim();
5161
- if (!title) throw new Error("Linear issue title is required");
5162
- const { viewer, teams } = await listLinearTeams(opts);
5163
- const team = resolveLinearTeam(teams, input.team);
5164
- const mutationInput = {
5165
- teamId: team.id,
5166
- title
5167
- };
5168
- const description = input.description?.trim();
5169
- if (description) mutationInput.description = description;
5170
- if (input.state?.trim()) {
5171
- mutationInput.stateId = resolveLinearState(team, input.state).id;
5172
- }
5173
- const parentRef = input.parent?.trim();
5174
- if (parentRef) {
5175
- const parent = await getLinearIssue(parentRef, opts);
5176
- mutationInput.parentId = parent.id;
5177
- }
5178
- const assignee = input.assignee === void 0 ? void 0 : input.assignee?.trim() || null;
5179
- if (assignee === "me") mutationInput.assigneeId = viewer.id;
5180
- else if (assignee) mutationInput.assigneeId = assignee;
5181
- const priority = normalizePriority(input.priority);
5182
- if (priority != null) mutationInput.priority = priority;
5183
- const json = await linearGraphql(ISSUE_CREATE, { input: mutationInput }, opts);
5184
- if (!json.issueCreate?.success || !json.issueCreate.issue) {
5185
- throw new Error("Linear issueCreate failed");
5186
- }
5187
- return mapIssue(json.issueCreate.issue);
5188
- }
5189
- async function updateLinearIssue(input, opts) {
5190
- const issueId = input.id.trim();
5191
- if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
5192
- const mutationInput = {};
5193
- if (input.title !== void 0) {
5194
- const title = input.title.trim();
5195
- if (!title) throw new Error("Linear issue title cannot be empty");
5196
- mutationInput.title = title;
3851
+ function isGitHubNoneToken(value) {
3852
+ if (value == null) return true;
3853
+ const s = value.trim();
3854
+ return !s || /^(none|clear|unschedule|null)$/i.test(s);
3855
+ }
3856
+ function githubRelationEditFlag(type, remove = false) {
3857
+ const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
3858
+ if (s === "blockedby" || s === "blocked" || s === "dependson") {
3859
+ return remove ? "--remove-blocked-by" : "--add-blocked-by";
5197
3860
  }
5198
- if (input.description !== void 0) {
5199
- mutationInput.description = input.description;
3861
+ if (s === "blocks" || s === "blocking") {
3862
+ return remove ? "--remove-blocking" : "--add-blocking";
5200
3863
  }
3864
+ throw new Error(
3865
+ `GitHub relation type must be blocks or blockedBy (got ${type}). Parent is a separate parent= field.`
3866
+ );
3867
+ }
3868
+ function labelsEqual(a, b) {
3869
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
3870
+ }
3871
+ async function updateGitHubIssue(input, opts) {
3872
+ const number = parseGitHubIssueNumber(input.id);
3873
+ const title = input.title?.trim();
3874
+ const body = input.body;
3875
+ const state = input.state?.trim().toLowerCase();
5201
3876
  const addRelations = input.relations?.length ? input.relations : void 0;
5202
3877
  const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
5203
- const needsIssue = Boolean(input.state?.trim()) || input.cycle !== void 0 || input.labels !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
5204
- const existing = needsIssue ? await getLinearIssue(issueId, opts) : void 0;
5205
- if (input.state?.trim()) {
5206
- const { teams } = await listLinearTeams(opts);
5207
- const teamRef = existing?.team?.key || existing?.team?.id;
5208
- const team = teamRef ? resolveLinearTeam(teams, teamRef) : void 0;
5209
- if (!team?.states.length) {
5210
- throw new Error(`Linear issue ${existing?.identifier ?? issueId} has no workflow states to resolve "${input.state}"`);
5211
- }
5212
- mutationInput.stateId = resolveLinearState(team, input.state).id;
5213
- }
5214
- if (input.cycle !== void 0) {
5215
- const teamId = existing?.team?.id;
5216
- if (!teamId) {
5217
- throw new Error(`Linear issue ${existing?.identifier ?? issueId} has no team to resolve a cycle`);
5218
- }
5219
- const cycles = await listLinearTeamCycles(teamId, opts);
5220
- mutationInput.cycleId = resolveLinearCycle(
5221
- { ...cycles, key: cycles.key || existing?.team?.key || teamId },
5222
- input.cycle
3878
+ const hasEditFields = Boolean(title) || body !== void 0 || input.labels !== void 0 || input.project !== void 0 || input.parent !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
3879
+ if (!hasEditFields && !state) {
3880
+ throw new Error(
3881
+ "github_update_issue needs at least one of title, body, state, labels, project, parent, relations, removeRelations"
5223
3882
  );
5224
3883
  }
5225
- if (input.assignee !== void 0) {
5226
- const assignee = input.assignee?.trim() || null;
5227
- if (assignee === "me") {
5228
- const { viewer } = await listLinearTeams(opts);
5229
- mutationInput.assigneeId = viewer.id;
5230
- } else {
5231
- mutationInput.assigneeId = assignee;
5232
- }
5233
- }
5234
- if (input.priority !== void 0) {
5235
- mutationInput.priority = normalizePriority(input.priority);
3884
+ const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
3885
+ if (state === "closed" || state === "close") {
3886
+ requireGhOk(
3887
+ await gh(["issue", "close", String(number), ...repoArgs], cwd, { reject: false }),
3888
+ `GitHub close #${number}`
3889
+ );
3890
+ } else if (state === "open" || state === "reopen") {
3891
+ requireGhOk(
3892
+ await gh(["issue", "reopen", String(number), ...repoArgs], cwd, { reject: false }),
3893
+ `GitHub reopen #${number}`
3894
+ );
3895
+ } else if (state) {
3896
+ throw new Error(`GitHub issue state must be open or closed (got ${input.state})`);
5236
3897
  }
3898
+ const args = ["issue", "edit", String(number), ...repoArgs];
3899
+ if (title) args.push("--title", title);
3900
+ if (body !== void 0) args.push("--body", body);
5237
3901
  if (input.labels !== void 0) {
5238
- mutationInput.labelIds = await resolveLinearLabelIds(input.labels, {
5239
- ...opts,
5240
- teamId: existing?.team?.id
5241
- });
3902
+ const existing = await getGitHubIssue(String(number), opts);
3903
+ const desired = input.labels.map((name) => name.trim()).filter(Boolean);
3904
+ const add = desired.filter((name) => !existing.labels.some((cur) => labelsEqual(cur, name)));
3905
+ const remove = existing.labels.filter((cur) => !desired.some((name) => labelsEqual(cur, name)));
3906
+ if (add.length) args.push("--add-label", add.join(","));
3907
+ if (remove.length) args.push("--remove-label", remove.join(","));
5242
3908
  }
5243
3909
  if (input.project !== void 0) {
5244
- mutationInput.projectId = await resolveLinearProjectId(input.project, opts);
3910
+ const current = await listGitHubIssueProjectNames(number, cwd, repoArgs);
3911
+ if (isGitHubNoneToken(input.project)) {
3912
+ for (const name of current) args.push("--remove-project", name);
3913
+ } else {
3914
+ const wanted = (input.project ?? "").trim();
3915
+ if (!current.some((name) => labelsEqual(name, wanted))) {
3916
+ args.push("--add-project", wanted);
3917
+ }
3918
+ for (const name of current) {
3919
+ if (!labelsEqual(name, wanted)) args.push("--remove-project", name);
3920
+ }
3921
+ }
5245
3922
  }
5246
3923
  if (input.parent !== void 0) {
5247
- if (isLinearNoneToken(input.parent)) {
5248
- mutationInput.parentId = null;
3924
+ if (isGitHubNoneToken(input.parent)) {
3925
+ args.push("--remove-parent");
5249
3926
  } else {
5250
- const parent = await getLinearIssue((input.parent ?? "").trim(), opts);
5251
- mutationInput.parentId = parent.id;
3927
+ args.push("--parent", String(parseGitHubIssueNumber(input.parent ?? "")));
5252
3928
  }
5253
3929
  }
5254
- const hasIssueFields = Object.keys(mutationInput).length > 0;
5255
- const hasRelationWrites = Boolean(addRelations || removeRelations);
5256
- if (!hasIssueFields && !hasRelationWrites) {
5257
- throw new Error(
5258
- "linear_update_issue needs at least one of title, description, state, assignee, priority, cycle, labels, project, parent, relations, removeRelations"
5259
- );
5260
- }
5261
- let updated;
5262
- if (hasIssueFields) {
5263
- const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
5264
- if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
5265
- throw new Error("Linear issueUpdate failed");
5266
- }
5267
- updated = mapIssue(json.issueUpdate.issue);
3930
+ for (const rel of removeRelations ?? []) {
3931
+ args.push(githubRelationEditFlag(rel.type, true), String(parseGitHubIssueNumber(rel.issue)));
5268
3932
  }
5269
- if (hasRelationWrites) {
5270
- const base = updated ?? existing;
5271
- if (!base) {
5272
- throw new Error(`Linear issue not found: ${issueId}`);
5273
- }
5274
- await applyLinearIssueRelations(base, addRelations, removeRelations, opts);
5275
- return getLinearIssue(base.id || issueId, opts);
3933
+ for (const rel of addRelations ?? []) {
3934
+ args.push(githubRelationEditFlag(rel.type, false), String(parseGitHubIssueNumber(rel.issue)));
5276
3935
  }
5277
- return updated;
5278
- }
5279
- async function commentLinearIssue(input, opts) {
5280
- const issueId = input.id.trim();
5281
- const body = input.body.trim();
5282
- if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
5283
- if (!body) throw new Error("Linear comment body is required");
5284
- const json = await linearGraphql(COMMENT_CREATE, { input: { issueId, body } }, opts);
5285
- if (!json.commentCreate?.success || !json.commentCreate.comment?.id) {
5286
- throw new Error("Linear commentCreate failed");
3936
+ const editChanged = args.length > 3 + repoArgs.length;
3937
+ if (editChanged) {
3938
+ requireGhOk(await gh(args, cwd, { reject: false }), `GitHub edit #${number}`);
5287
3939
  }
5288
- return {
5289
- id: String(json.commentCreate.comment.id),
5290
- body: String(json.commentCreate.comment.body ?? body),
5291
- url: json.commentCreate.comment.url?.trim() || void 0
5292
- };
3940
+ return getGitHubIssue(String(number), opts);
5293
3941
  }
5294
- async function validateLinearApiKey(apiKey) {
5295
- const key = apiKey.trim();
5296
- if (!key) return false;
5297
- try {
5298
- await listLinearIssuesDirect({ limit: 1, apiKey: key });
5299
- return true;
5300
- } catch {
5301
- return false;
3942
+ async function createGitHubIssue(input, opts) {
3943
+ const title = input.title.trim();
3944
+ if (!title) throw new Error("GitHub issue title is required");
3945
+ const parent = input.parent?.trim();
3946
+ const parentNumber = parent ? parseGitHubIssueNumber(parent) : null;
3947
+ const bodyParts = [
3948
+ parentNumber ? `Spin-off of #${parentNumber}.` : null,
3949
+ input.body?.trim() || null
3950
+ ].filter(Boolean);
3951
+ const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
3952
+ const args = ["issue", "create", ...repoArgs, "--title", title];
3953
+ if (bodyParts.length) args.push("--body", bodyParts.join("\n\n"));
3954
+ const created = await gh([...args, "--json", "number,url,title"], cwd, { reject: false });
3955
+ if (created.exitCode === 0 && created.stdout.trim()) {
3956
+ try {
3957
+ const parsed = JSON.parse(created.stdout);
3958
+ if (parsed.number) return getGitHubIssue(String(parsed.number), opts);
3959
+ } catch {
3960
+ }
5302
3961
  }
3962
+ const fallback = created.exitCode === 0 ? created : await gh(args, cwd, { reject: false });
3963
+ const stdout = requireGhOk(fallback, "GitHub create issue");
3964
+ const url = stdout.trim().match(/https?:\/\/github\.com\/[^/\s]+\/[^/\s]+\/issues\/(\d+)/i);
3965
+ if (url?.[1]) return getGitHubIssue(url[1], opts);
3966
+ throw new Error(`GitHub create issue: could not parse issue from ${stdout.trim() || "empty output"}`);
5303
3967
  }
5304
3968
 
5305
3969
  // src/integrations/issues.ts
@@ -6524,7 +5188,7 @@ function runRemoteHost(opts) {
6524
5188
  }
6525
5189
 
6526
5190
  // src/relay/server.ts
6527
- import { createServer as createServer2 } from "http";
5191
+ import { createServer } from "http";
6528
5192
  import { WebSocketServer } from "ws";
6529
5193
 
6530
5194
  // src/slack/oauth-redirect.ts
@@ -6933,7 +5597,7 @@ function connectSession(ws, opts, log) {
6933
5597
  }
6934
5598
 
6935
5599
  // src/slack/relay-protocol.ts
6936
- import { randomBytes as randomBytes3 } from "crypto";
5600
+ import { randomBytes as randomBytes2 } from "crypto";
6937
5601
  var SLACK_RELAY_PING_INTERVAL_MS = 2e4;
6938
5602
  var SLACK_RELAY_PONG_TIMEOUT_MS = 15e3;
6939
5603
  function slackRelaySessionKey(teamId, userId, deviceId) {
@@ -6943,7 +5607,7 @@ function slackRelayUserKey(teamId, userId) {
6943
5607
  return `${teamId.trim()}:${userId.trim()}`;
6944
5608
  }
6945
5609
  function newSlackRelayEventId() {
6946
- return randomBytes3(12).toString("hex");
5610
+ return randomBytes2(12).toString("hex");
6947
5611
  }
6948
5612
  function parseSlackRelayClientMessage(raw) {
6949
5613
  try {
@@ -7471,7 +6135,7 @@ async function tryServeStatic(req, res, root) {
7471
6135
  }
7472
6136
 
7473
6137
  // src/remote/hub.ts
7474
- import { randomBytes as randomBytes4 } from "crypto";
6138
+ import { randomBytes as randomBytes3 } from "crypto";
7475
6139
  var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
7476
6140
  var CODE_TTL_MS = 10 * 60 * 1e3;
7477
6141
  function send(socket, msg) {
@@ -7481,7 +6145,7 @@ function send(socket, msg) {
7481
6145
  }
7482
6146
  }
7483
6147
  function newCode() {
7484
- const bytes = randomBytes4(6);
6148
+ const bytes = randomBytes3(6);
7485
6149
  let code = "";
7486
6150
  for (let i = 0; i < 6; i++) code += CODE_ALPHABET[bytes[i] % CODE_ALPHABET.length];
7487
6151
  return code;
@@ -7752,7 +6416,7 @@ var RemoteHub = class {
7752
6416
  host.phone = null;
7753
6417
  }
7754
6418
  const created = sessionToken == null;
7755
- const token = sessionToken ?? randomBytes4(24).toString("hex");
6419
+ const token = sessionToken ?? randomBytes3(24).toString("hex");
7756
6420
  if (host.sessionToken && host.sessionToken !== token) {
7757
6421
  this.phonesByToken.delete(host.sessionToken);
7758
6422
  }
@@ -7802,7 +6466,7 @@ var RemoteHub = class {
7802
6466
  };
7803
6467
 
7804
6468
  // src/relay/accounts.ts
7805
- import { createHash as createHash2, randomBytes as randomBytes5, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
6469
+ import { createHash, randomBytes as randomBytes4, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
7806
6470
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, realpathSync } from "fs";
7807
6471
  import { createRequire } from "module";
7808
6472
  import { dirname, join as join3 } from "path";
@@ -7822,7 +6486,7 @@ function openDatabase(filePath) {
7822
6486
  return db;
7823
6487
  }
7824
6488
  function sha2562(value) {
7825
- return createHash2("sha256").update(value).digest("hex");
6489
+ return createHash("sha256").update(value).digest("hex");
7826
6490
  }
7827
6491
  function sameHash(a, b) {
7828
6492
  const left = Buffer.from(a);
@@ -8054,7 +6718,7 @@ var RelayAccountStore = class {
8054
6718
  ).run(issuer, subject, accountId, email, verified ? 1 : 0);
8055
6719
  }
8056
6720
  mintCredential(accountId) {
8057
- const credential = randomBytes5(32).toString("hex");
6721
+ const credential = randomBytes4(32).toString("hex");
8058
6722
  this.db.prepare("INSERT INTO credentials (token_hash, account_id, created_at) VALUES (?, ?, ?)").run(sha2562(credential), accountId, Date.now());
8059
6723
  return credential;
8060
6724
  }
@@ -8142,7 +6806,7 @@ function cursorGitHostEntry() {
8142
6806
  }
8143
6807
 
8144
6808
  // src/relay/account-entire.ts
8145
- import { createHash as createHash3, randomBytes as randomBytes6 } from "crypto";
6809
+ import { createHash as createHash2, randomBytes as randomBytes5 } from "crypto";
8146
6810
  var ENTIRE_AUTHORIZE_ORIGIN = "https://auth.entire.io";
8147
6811
  var ENTIRE_DEFAULT_ISSUER = "https://us.auth.entire.io";
8148
6812
  var ISSUER_SUFFIX = ".auth.entire.io";
@@ -8165,10 +6829,10 @@ function entireAccountIssuer(value) {
8165
6829
  return `https://${host}`;
8166
6830
  }
8167
6831
  function newEntireCodeVerifier() {
8168
- return randomBytes6(32).toString("base64url");
6832
+ return randomBytes5(32).toString("base64url");
8169
6833
  }
8170
6834
  function entireCodeChallenge(verifier) {
8171
- return createHash3("sha256").update(verifier).digest("base64url");
6835
+ return createHash2("sha256").update(verifier).digest("base64url");
8172
6836
  }
8173
6837
  function entireAuthorizeUrl(clientId, redirectUri, state, codeChallenge) {
8174
6838
  const url = new URL("/authorize", ENTIRE_AUTHORIZE_ORIGIN);
@@ -8754,7 +7418,7 @@ async function startRelayServer(opts) {
8754
7418
  sendJson2(res, 200, { ok: true, ...value.payload });
8755
7419
  return true;
8756
7420
  };
8757
- const httpServer = createServer2((req, res) => {
7421
+ const httpServer = createServer((req, res) => {
8758
7422
  void (async () => {
8759
7423
  const reqUrl = req.url || "/";
8760
7424
  if (accountStore && await handleRelayAccountHttp({
@@ -9020,7 +7684,7 @@ async function startHeadlessHost(opts) {
9020
7684
  };
9021
7685
  const refreshRunning = async () => {
9022
7686
  try {
9023
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-7VTAUY24.js");
7687
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-7FVDBE7Y.js");
9024
7688
  agentsRunning = getOrchestrator2().getRuntime().running;
9025
7689
  } catch {
9026
7690
  agentsRunning = 0;
@@ -11163,7 +9827,7 @@ async function handleJobStop(params, _ctx) {
11163
9827
  return stopDetachedJob(jobCwd(rec), id, reason ? { reason } : void 0);
11164
9828
  }
11165
9829
  async function defaultTurnWait(ref, timeoutMs) {
11166
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-7VTAUY24.js");
9830
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-7FVDBE7Y.js");
11167
9831
  const orch = getOrchestrator2();
11168
9832
  const thread = await orch.waitForTurn(ref, timeoutMs, {
11169
9833
  resolveIfStillRunning: true
@@ -11516,7 +10180,7 @@ async function startMcpServer() {
11516
10180
  );
11517
10181
  }
11518
10182
  try {
11519
- const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-XIF4EC4K.js");
10183
+ const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-RXBH7KY4.js");
11520
10184
  orch.setMaxConcurrent(maxConcurrentAgents2());
11521
10185
  } catch {
11522
10186
  }
@@ -12064,7 +10728,7 @@ async function startMcpServer() {
12064
10728
  if (!worktreeProfile) {
12065
10729
  registerConnectedIssueVendorTools(server);
12066
10730
  const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-ZNUEZGHB.js");
12067
- const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-XIF4EC4K.js");
10731
+ const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-RXBH7KY4.js");
12068
10732
  const accountDefaults = resolveThreadDefaults2();
12069
10733
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
12070
10734
  server.tool(
@@ -12748,8 +11412,8 @@ async function startMcpServer() {
12748
11412
  }
12749
11413
 
12750
11414
  // src/agent-rpc/server.ts
12751
- import { randomBytes as randomBytes7, randomUUID as randomUUID8, timingSafeEqual as timingSafeEqual2 } from "crypto";
12752
- import { createServer as createServer3 } from "http";
11415
+ import { randomBytes as randomBytes6, randomUUID as randomUUID8, timingSafeEqual as timingSafeEqual2 } from "crypto";
11416
+ import { createServer as createServer2 } from "http";
12753
11417
  import { WebSocketServer as WebSocketServer2 } from "ws";
12754
11418
  function tokensEqual(a, b) {
12755
11419
  const left = Buffer.from(a);
@@ -12788,7 +11452,7 @@ var AgentRpcServer = class {
12788
11452
  async listen(opts = {}) {
12789
11453
  const host = opts.host ?? "127.0.0.1";
12790
11454
  const port = opts.port ?? 0;
12791
- const authToken = randomBytes7(24).toString("base64url");
11455
+ const authToken = randomBytes6(24).toString("base64url");
12792
11456
  this.meta = {
12793
11457
  runtimeId: randomUUID8(),
12794
11458
  pid: process.pid,
@@ -12797,7 +11461,7 @@ var AgentRpcServer = class {
12797
11461
  startedAt: Date.now(),
12798
11462
  transports: []
12799
11463
  };
12800
- const http = createServer3((_req, res) => {
11464
+ const http = createServer2((_req, res) => {
12801
11465
  res.statusCode = 404;
12802
11466
  res.end();
12803
11467
  });
@@ -13641,7 +12305,7 @@ function slackTokenFor(ws, kind = "read") {
13641
12305
  }
13642
12306
 
13643
12307
  // src/slack/oauth.ts
13644
- import { randomBytes as randomBytes8 } from "crypto";
12308
+ import { randomBytes as randomBytes7 } from "crypto";
13645
12309
  var SLACK_BOT_SCOPES = [
13646
12310
  "app_mentions:read",
13647
12311
  "channels:history",
@@ -13734,7 +12398,7 @@ async function startSlackOAuth(opts) {
13734
12398
  throw new SlackOAuthCancelledError();
13735
12399
  }
13736
12400
  const { clientId } = slackOAuthCredentials();
13737
- const state = randomBytes8(16).toString("hex");
12401
+ const state = randomBytes7(16).toString("hex");
13738
12402
  const authorizeUrl = slackOAuthAuthorizeUrl(clientId, state);
13739
12403
  const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
13740
12404
  const pollIntervalMs = opts?.pollIntervalMs ?? 400;
@@ -14627,7 +13291,7 @@ function resolveSlackListenMode(opts) {
14627
13291
  }
14628
13292
 
14629
13293
  // src/relay/account-login.ts
14630
- import { randomBytes as randomBytes9 } from "crypto";
13294
+ import { randomBytes as randomBytes8 } from "crypto";
14631
13295
  var RemoteAccountLoginCancelled = class extends Error {
14632
13296
  constructor() {
14633
13297
  super("Sign-in cancelled");
@@ -14693,7 +13357,7 @@ async function readJson3(res, url) {
14693
13357
  async function startRemoteAccountLogin(opts) {
14694
13358
  if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
14695
13359
  const origin = (opts.origin ?? remoteAccountHttpOrigin()).replace(/\/+$/, "");
14696
- const state = randomBytes9(16).toString("hex");
13360
+ const state = randomBytes8(16).toString("hex");
14697
13361
  const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
14698
13362
  const pollIntervalMs = opts.pollIntervalMs ?? 400;
14699
13363
  const linkToken = opts.linkToken?.trim() ?? "";
@@ -14831,7 +13495,7 @@ async function continueSourceThread(threadId, prompt) {
14831
13495
  await continueOnReply(threadId, prompt);
14832
13496
  return;
14833
13497
  }
14834
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-7VTAUY24.js");
13498
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-7FVDBE7Y.js");
14835
13499
  await getOrchestrator2().send(threadId, prompt);
14836
13500
  } catch {
14837
13501
  }
@@ -16115,6 +14779,7 @@ export {
16115
14779
  stripBrightsyNdjsonNoise,
16116
14780
  stripNestedElectronEnv,
16117
14781
  submitPrStack,
14782
+ subtractUsage,
16118
14783
  suggestSlug,
16119
14784
  sumUsageList,
16120
14785
  summarizeConversation,