@band-ai/sdk 0.1.2

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 (91) hide show
  1. package/dist/CodexAdapter-A1k_LMdZ.d.ts +1278 -0
  2. package/dist/CodexAdapter-BbZD-nBd.d.cts +1278 -0
  3. package/dist/adapters.cjs +10643 -0
  4. package/dist/adapters.d.cts +217 -0
  5. package/dist/adapters.d.ts +217 -0
  6. package/dist/adapters.js +92 -0
  7. package/dist/backends-BnOLsG7w.d.cts +20 -0
  8. package/dist/backends-C4n6cKOf.d.ts +20 -0
  9. package/dist/chunk-3BKAC4OZ.js +44 -0
  10. package/dist/chunk-5CXIMAH2.js +52 -0
  11. package/dist/chunk-6AJA2OPC.js +3682 -0
  12. package/dist/chunk-A3TDK6GP.js +6674 -0
  13. package/dist/chunk-AVVDPXP4.js +39 -0
  14. package/dist/chunk-EDPKUUJL.js +177 -0
  15. package/dist/chunk-FUODYQRE.js +2562 -0
  16. package/dist/chunk-GVEQ3RAH.js +373 -0
  17. package/dist/chunk-INA6YZTZ.js +88 -0
  18. package/dist/chunk-IZPV6QS6.js +115 -0
  19. package/dist/chunk-LY55KYVI.js +0 -0
  20. package/dist/chunk-NIAQBANR.js +577 -0
  21. package/dist/chunk-OD2G5LFQ.js +78 -0
  22. package/dist/chunk-SFVKOEQH.js +103 -0
  23. package/dist/chunk-SG4WLD2H.js +1093 -0
  24. package/dist/chunk-YR3BOQNW.js +39 -0
  25. package/dist/chunk-ZZJRRBPQ.js +82 -0
  26. package/dist/claude-CcnQ5OUC.d.ts +53 -0
  27. package/dist/claude-CwTAxhwI.d.cts +53 -0
  28. package/dist/config.cjs +164 -0
  29. package/dist/config.d.cts +19 -0
  30. package/dist/config.d.ts +19 -0
  31. package/dist/config.js +9 -0
  32. package/dist/converters.cjs +1086 -0
  33. package/dist/converters.d.cts +143 -0
  34. package/dist/converters.d.ts +143 -0
  35. package/dist/converters.js +468 -0
  36. package/dist/core.cjs +185 -0
  37. package/dist/core.d.cts +22 -0
  38. package/dist/core.d.ts +22 -0
  39. package/dist/core.js +24 -0
  40. package/dist/customTools-BzF0IISO.d.cts +10 -0
  41. package/dist/customTools-BzF0IISO.d.ts +10 -0
  42. package/dist/dtos-DUpbIu8k.d.cts +163 -0
  43. package/dist/dtos-DUpbIu8k.d.ts +163 -0
  44. package/dist/history-ByessXNq.d.ts +24 -0
  45. package/dist/history-i6yN7neC.d.cts +24 -0
  46. package/dist/index.cjs +13914 -0
  47. package/dist/index.d.cts +63 -0
  48. package/dist/index.d.ts +63 -0
  49. package/dist/index.js +198 -0
  50. package/dist/linear.cjs +2207 -0
  51. package/dist/linear.d.cts +167 -0
  52. package/dist/linear.d.ts +167 -0
  53. package/dist/linear.js +2139 -0
  54. package/dist/logger-CABQOnlR.d.cts +21 -0
  55. package/dist/logger-CABQOnlR.d.ts +21 -0
  56. package/dist/mcp-claude.cjs +584 -0
  57. package/dist/mcp-claude.d.cts +4 -0
  58. package/dist/mcp-claude.d.ts +4 -0
  59. package/dist/mcp-claude.js +11 -0
  60. package/dist/mcp.cjs +1247 -0
  61. package/dist/mcp.d.cts +97 -0
  62. package/dist/mcp.d.ts +97 -0
  63. package/dist/mcp.js +27 -0
  64. package/dist/opencode-BBcDchcN.d.cts +159 -0
  65. package/dist/opencode-DthQQUsZ.d.ts +159 -0
  66. package/dist/pagination-BPiys10t.d.cts +25 -0
  67. package/dist/pagination-CcDkVFxK.d.ts +25 -0
  68. package/dist/protocols-CInhtFpA.d.cts +116 -0
  69. package/dist/protocols-DrhzOH6K.d.ts +116 -0
  70. package/dist/rest.cjs +1159 -0
  71. package/dist/rest.d.cts +144 -0
  72. package/dist/rest.d.ts +144 -0
  73. package/dist/rest.js +18 -0
  74. package/dist/runtime.cjs +5353 -0
  75. package/dist/runtime.d.cts +276 -0
  76. package/dist/runtime.d.ts +276 -0
  77. package/dist/runtime.js +274 -0
  78. package/dist/schemas-BpEBkq5V.d.cts +592 -0
  79. package/dist/schemas-vos1AaBc.d.ts +592 -0
  80. package/dist/sdk-HJUVSSWA.js +10 -0
  81. package/dist/simpleAdapter-9OE_p6QW.d.ts +29 -0
  82. package/dist/simpleAdapter-DqhvjsGr.d.cts +29 -0
  83. package/dist/testing.cjs +217 -0
  84. package/dist/testing.d.cts +126 -0
  85. package/dist/testing.d.ts +126 -0
  86. package/dist/testing.js +186 -0
  87. package/dist/types-7jVzOeOP.d.ts +446 -0
  88. package/dist/types-CSHFipIc.d.cts +331 -0
  89. package/dist/types-CovcHpYf.d.cts +446 -0
  90. package/dist/types-CxII3XkA.d.ts +331 -0
  91. package/package.json +189 -0
package/dist/linear.js ADDED
@@ -0,0 +1,2139 @@
1
+ import {
2
+ NoopLogger
3
+ } from "./chunk-OD2G5LFQ.js";
4
+ import {
5
+ UnsupportedFeatureError
6
+ } from "./chunk-YR3BOQNW.js";
7
+
8
+ // src/integrations/linear/bridge/handler.ts
9
+ import { LinearDocument as L2 } from "@linear/sdk";
10
+
11
+ // src/integrations/linear/handles.ts
12
+ function stripHandlePrefix(handle) {
13
+ return handle.trim().replace(/^@+/, "").toLowerCase();
14
+ }
15
+ function dedupeHandles(handles) {
16
+ const deduped = [];
17
+ const seen = /* @__PURE__ */ new Set();
18
+ for (const handle of handles) {
19
+ const normalized = stripHandlePrefix(handle);
20
+ if (!normalized || seen.has(normalized)) {
21
+ continue;
22
+ }
23
+ seen.add(normalized);
24
+ deduped.push(normalized);
25
+ }
26
+ return deduped;
27
+ }
28
+
29
+ // src/integrations/linear/activities.ts
30
+ import { LinearDocument as L } from "@linear/sdk";
31
+ async function postActivity(client, sessionId, content) {
32
+ await client.createAgentActivity({
33
+ agentSessionId: sessionId,
34
+ content
35
+ });
36
+ }
37
+ async function postBodyActivity(client, sessionId, type, body) {
38
+ await postActivity(client, sessionId, { type, body });
39
+ }
40
+ async function postThought(client, sessionId, body) {
41
+ await postBodyActivity(client, sessionId, L.AgentActivityType.Thought, body);
42
+ }
43
+ async function postError(client, sessionId, body) {
44
+ await postBodyActivity(client, sessionId, L.AgentActivityType.Error, body);
45
+ }
46
+ async function postResponse(client, sessionId, body) {
47
+ await postBodyActivity(client, sessionId, L.AgentActivityType.Response, body);
48
+ }
49
+ async function postElicitation(client, sessionId, body) {
50
+ await postBodyActivity(client, sessionId, L.AgentActivityType.Elicitation, body);
51
+ }
52
+ async function postAction(client, sessionId, body) {
53
+ await postActivity(client, sessionId, {
54
+ type: L.AgentActivityType.Action,
55
+ action: body,
56
+ parameter: ""
57
+ });
58
+ }
59
+ async function updatePlan(client, sessionId, steps) {
60
+ const planSummary = steps.map((step) => {
61
+ const icon = step.status === "completed" ? "\u2705" : step.status === "in_progress" ? "\u23F3" : step.status === "failed" ? "\u274C" : "\u2B1C";
62
+ return `${icon} ${step.title}`;
63
+ }).join("\n");
64
+ await postActivity(client, sessionId, {
65
+ type: L.AgentActivityType.Thought,
66
+ body: `**Plan:**
67
+ ${planSummary}`
68
+ });
69
+ }
70
+
71
+ // src/integrations/linear/bridge/message.ts
72
+ function buildBridgeMessage(input) {
73
+ const header = input.action === "created" ? "[Linear]: Agent session created." : "[Linear]: Agent session updated.";
74
+ const userRequest = firstNonEmpty(
75
+ input.commentBody,
76
+ input.promptContext,
77
+ input.issueTitle ? `Please handle ${input.issueTitle}.` : null
78
+ ) ?? "Please handle this Linear request.";
79
+ const issueIdLine = input.issueId ? `issue_id: ${input.issueId}` : "issue_id: none";
80
+ const issueIdentifierLine = input.issueIdentifier ? `issue_identifier: ${input.issueIdentifier}` : "issue_identifier: none";
81
+ const issueTitleLine = input.issueTitle ? `issue_title: ${input.issueTitle}` : "issue_title: none";
82
+ const issueUrlLine = input.issueUrl ? `issue_url: ${input.issueUrl}` : "issue_url: none";
83
+ const issueTeamLine = firstNonEmpty(input.issueTeamKey, input.issueTeamName) ? `issue_team: ${firstNonEmpty(input.issueTeamKey, input.issueTeamName)}` : "issue_team: none";
84
+ const issueTeamIdLine = input.issueTeamId ? `issue_team_id: ${input.issueTeamId}` : "issue_team_id: none";
85
+ const issueStateLine = firstNonEmpty(input.issueStateName, input.issueStateType) ? `issue_state: ${firstNonEmpty(input.issueStateName, input.issueStateType)}` : "issue_state: none";
86
+ const issueStateIdLine = input.issueStateId ? `issue_state_id: ${input.issueStateId}` : "issue_state_id: none";
87
+ const issueStateTypeLine = input.issueStateType ? `issue_state_type: ${input.issueStateType}` : "issue_state_type: none";
88
+ const issueAssigneeLine = input.issueAssigneeName ? `issue_assignee: ${input.issueAssigneeName}` : "issue_assignee: none";
89
+ const issueAssigneeIdLine = input.issueAssigneeId ? `issue_assignee_id: ${input.issueAssigneeId}` : "issue_assignee_id: none";
90
+ const sessionStatusLine = input.sessionStatus ? `session_status: ${input.sessionStatus}` : "session_status: none";
91
+ const sessionTypeLine = input.sessionType ? `session_type: ${input.sessionType}` : "session_type: none";
92
+ const sessionCreatedLine = input.sessionCreatedAt ? `session_created_at: ${input.sessionCreatedAt}` : "session_created_at: none";
93
+ const sessionUpdatedLine = input.sessionUpdatedAt ? `session_updated_at: ${input.sessionUpdatedAt}` : "session_updated_at: none";
94
+ const appUserLine = input.appUserId ? `app_user_id: ${input.appUserId}` : "app_user_id: none";
95
+ const organizationLine = input.organizationId ? `organization_id: ${input.organizationId}` : "organization_id: none";
96
+ const oauthClientLine = input.oauthClientId ? `oauth_client_id: ${input.oauthClientId}` : "oauth_client_id: none";
97
+ const webhookIdLine = input.webhookId ? `webhook_id: ${input.webhookId}` : "webhook_id: none";
98
+ const webhookTimestampLine = typeof input.webhookTimestamp === "number" ? `webhook_timestamp: ${input.webhookTimestamp}` : "webhook_timestamp: none";
99
+ const commentIdLine = input.commentId ? `comment_id: ${input.commentId}` : "comment_id: none";
100
+ const promptContext = firstNonEmpty(input.promptContext) ?? "none";
101
+ const issueDescription = firstNonEmpty(input.issueDescription) ?? "none";
102
+ const commentBody = firstNonEmpty(input.commentBody) ?? "none";
103
+ const suggestedPeersLine = input.suggestedPeerHandles.length > 0 ? input.suggestedPeerHandles.map((handle) => ` - ${handle}`).join("\n") : " - none";
104
+ return `${userRequest}
105
+
106
+ ${header}
107
+
108
+ Linear session context:
109
+ - session_id: ${input.sessionId}
110
+ - ${sessionStatusLine}
111
+ - ${sessionTypeLine}
112
+ - ${sessionCreatedLine}
113
+ - ${sessionUpdatedLine}
114
+ - ${issueIdLine}
115
+ - ${issueIdentifierLine}
116
+ - ${issueTitleLine}
117
+ - ${issueUrlLine}
118
+ - ${issueTeamLine}
119
+ - ${issueTeamIdLine}
120
+ - ${issueStateLine}
121
+ - ${issueStateIdLine}
122
+ - ${issueStateTypeLine}
123
+ - ${issueAssigneeLine}
124
+ - ${issueAssigneeIdLine}
125
+ - inferred_session_intent: ${input.sessionIntent}
126
+ - writeback_mode: ${input.writebackMode}
127
+ - ${appUserLine}
128
+ - ${organizationLine}
129
+ - ${oauthClientLine}
130
+ - ${webhookIdLine}
131
+ - ${webhookTimestampLine}
132
+
133
+ Bridge responsibilities:
134
+ - own orchestration for this Linear session
135
+ - decide whether you can answer alone or need help
136
+ - discover and invite specialists only when needed
137
+ - keep Linear updated with meaningful milestones
138
+ - use linear_post_thought and linear_post_action for meaningful progress updates
139
+ - call linear_post_response when the work is actually finished
140
+ - when you delegate inside Thenvoi, include the relevant ticket context in the room message so the specialist can act without hidden state
141
+
142
+ Suggested peers available in the registry right now. They are not in the room yet:
143
+ ${suggestedPeersLine}
144
+
145
+ Prompt context:
146
+ ${promptContext}
147
+
148
+ Issue description:
149
+ ${issueDescription}
150
+
151
+ Linked comment:
152
+ - ${commentIdLine}
153
+ ${commentBody}`;
154
+ }
155
+ function extractPromptedResponseBody(payload) {
156
+ if (typeof payload !== "object" || payload === null) {
157
+ return "";
158
+ }
159
+ const record = payload;
160
+ const activity = record.agentActivity;
161
+ if (typeof activity !== "object" || activity === null) {
162
+ return "";
163
+ }
164
+ const content = activity.content;
165
+ if (typeof content !== "object" || content === null) {
166
+ return "";
167
+ }
168
+ const body = content.body;
169
+ return typeof body === "string" ? body : "";
170
+ }
171
+ function detectSessionIntent(input) {
172
+ if (input.issueStateType?.trim().toLowerCase() === "started") {
173
+ return "implementation";
174
+ }
175
+ const explicitDirective = [
176
+ input.promptContext,
177
+ input.commentBody
178
+ ].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase();
179
+ if (/(please implement|implement (?:this|it|now|the)|build (?:this|it|now|the)|code (?:this|it|now|the)|ship (?:it|this)|start coding|start implementation|tighten|adjust|refine)/.test(explicitDirective)) {
180
+ return "implementation";
181
+ }
182
+ return "planning";
183
+ }
184
+ function extractIssueTeamKey(issue) {
185
+ const team = extractNestedRecord(issue, "team");
186
+ return typeof team?.key === "string" ? team.key : null;
187
+ }
188
+ function extractIssueTeamName(issue) {
189
+ const team = extractNestedRecord(issue, "team");
190
+ return typeof team?.name === "string" ? team.name : null;
191
+ }
192
+ function extractIssueTeamId(issue) {
193
+ const issueRecord = extractRecord(issue);
194
+ if (typeof issueRecord?.teamId === "string") {
195
+ return issueRecord.teamId;
196
+ }
197
+ const team = extractNestedRecord(issue, "team");
198
+ return typeof team?.id === "string" ? team.id : null;
199
+ }
200
+ function extractIssueStateField(issue, field) {
201
+ const state = extractNestedRecord(issue, "state");
202
+ const value = state?.[field];
203
+ return typeof value === "string" ? value : null;
204
+ }
205
+ function extractIssueAssigneeField(issue, field) {
206
+ const assignee = extractNestedRecord(issue, "assignee");
207
+ const value = assignee?.[field];
208
+ return typeof value === "string" ? value : null;
209
+ }
210
+ function firstNonEmpty(...values) {
211
+ for (const value of values) {
212
+ if (!value) {
213
+ continue;
214
+ }
215
+ const trimmed = value.trim();
216
+ if (trimmed.length > 0) {
217
+ return trimmed;
218
+ }
219
+ }
220
+ return null;
221
+ }
222
+ function extractNestedRecord(value, key) {
223
+ const record = extractRecord(value);
224
+ const nested = record?.[key];
225
+ return extractRecord(nested);
226
+ }
227
+ function extractRecord(value) {
228
+ return typeof value === "object" && value !== null ? value : null;
229
+ }
230
+
231
+ // src/integrations/linear/bridge/handler.ts
232
+ var SUPPORTED_ACTIONS = /* @__PURE__ */ new Set(["created", "updated", "canceled", "prompted"]);
233
+ var MAX_PEER_LOOKUP_PAGES = 25;
234
+ var PEER_PAGE_SIZE = 100;
235
+ var RECOVERED_ROOM_EVENT_RETRY_LIMIT = 2;
236
+ var RECOVERED_ROOM_EVENT_RETRY_BASE_DELAY_MS = 1e3;
237
+ function createLinearBridgeRuntime() {
238
+ return {
239
+ roomResolutionLocks: /* @__PURE__ */ new Map(),
240
+ resolvedHostHandleCache: /* @__PURE__ */ new WeakMap(),
241
+ authenticatedHostHandleCache: /* @__PURE__ */ new WeakMap()
242
+ };
243
+ }
244
+ function getAgentSessionEventKey(payload) {
245
+ return [
246
+ payload.agentSession.id,
247
+ normalizeAction(payload.action) ?? String(payload.action ?? "unknown"),
248
+ payload.agentSession.updatedAt ?? payload.agentSession.createdAt ?? "",
249
+ typeof payload.webhookTimestamp === "number" ? String(payload.webhookTimestamp) : ""
250
+ ].join(":");
251
+ }
252
+ async function handleAgentSessionEvent(input, options) {
253
+ const logger = input.deps.logger ?? new NoopLogger();
254
+ const config = normalizeConfig(input.config);
255
+ const action = normalizeAction(input.payload.action);
256
+ const runtime = options?.runtime ?? createLinearBridgeRuntime();
257
+ if (!action) {
258
+ logger.warn("linear_thenvoi_bridge.ignored_unknown_action", {
259
+ action: input.payload.action,
260
+ sessionId: input.payload.agentSession.id
261
+ });
262
+ return;
263
+ }
264
+ const sessionId = input.payload.agentSession.id;
265
+ const issueId = input.payload.agentSession.issueId ?? input.payload.agentSession.issue?.id ?? null;
266
+ const eventKey = options?.expectedEventKey ?? getAgentSessionEventKey(input.payload);
267
+ const existingBySession = await input.deps.store.getBySessionId(sessionId);
268
+ if (existingBySession?.lastEventKey === eventKey) {
269
+ logger.info("linear_thenvoi_bridge.duplicate_event_skipped", {
270
+ sessionId,
271
+ issueId,
272
+ action,
273
+ eventKey
274
+ });
275
+ return;
276
+ }
277
+ if (action === "canceled") {
278
+ await handleCanceledAction({
279
+ deps: input.deps,
280
+ logger,
281
+ sessionId,
282
+ issueId,
283
+ eventKey,
284
+ payloadAction: input.payload.action,
285
+ skipRoomWrite: options?.skipRoomWrite ?? false
286
+ });
287
+ return;
288
+ }
289
+ if (action === "prompted") {
290
+ await handlePromptedAction({
291
+ deps: input.deps,
292
+ config,
293
+ logger,
294
+ sessionId,
295
+ eventKey,
296
+ payload: input.payload,
297
+ skipRoomWrite: options?.skipRoomWrite ?? false
298
+ });
299
+ return;
300
+ }
301
+ if (action === "created" && !options?.skipAcknowledgment) {
302
+ try {
303
+ await postThought(input.deps.linearClient, sessionId, "Received session. Setting up workspace...");
304
+ } catch (ackError) {
305
+ logger.warn("linear_thenvoi_bridge.acknowledgment_failed", {
306
+ sessionId,
307
+ error: ackError instanceof Error ? ackError.message : String(ackError)
308
+ });
309
+ }
310
+ }
311
+ let roomRecord = null;
312
+ try {
313
+ const hostAgentHandle = await resolveHostAgentHandle({
314
+ thenvoiRest: input.deps.thenvoiRest,
315
+ configuredHostHandle: config.hostAgentHandle,
316
+ logger,
317
+ runtime
318
+ });
319
+ const existingByIssue = config.roomStrategy === "issue" && issueId ? await input.deps.store.getByIssueId(issueId) : null;
320
+ roomRecord = await resolveRoomRecord({
321
+ thenvoiRest: input.deps.thenvoiRest,
322
+ store: input.deps.store,
323
+ roomStrategy: config.roomStrategy,
324
+ sessionId,
325
+ issueId,
326
+ logger,
327
+ runtime
328
+ });
329
+ const sessionIntent = detectSessionIntent({
330
+ issueStateType: extractIssueStateField(input.payload.agentSession.issue, "type"),
331
+ promptContext: input.payload.promptContext,
332
+ commentBody: input.payload.agentSession.comment?.body,
333
+ issueTitle: input.payload.agentSession.issue?.title,
334
+ issueDescription: input.payload.agentSession.issue?.description
335
+ });
336
+ const suggestedPeerHandles = await selectRelevantPeerHandles({
337
+ thenvoiRest: input.deps.thenvoiRest,
338
+ roomId: roomRecord.thenvoiRoomId,
339
+ intent: sessionIntent,
340
+ hostAgentHandle,
341
+ planningAgentHandles: input.config.planningAgentHandles,
342
+ implementationAgentHandles: input.config.implementationAgentHandles,
343
+ logger
344
+ });
345
+ await ensureRoomParticipants({
346
+ thenvoiRest: input.deps.thenvoiRest,
347
+ roomId: roomRecord.thenvoiRoomId,
348
+ handles: [hostAgentHandle],
349
+ logger
350
+ });
351
+ const message = buildBridgeMessage({
352
+ sessionId,
353
+ issueId,
354
+ issueIdentifier: input.payload.agentSession.issue?.identifier,
355
+ sessionStatus: input.payload.agentSession.status,
356
+ sessionType: input.payload.agentSession.type,
357
+ sessionCreatedAt: input.payload.agentSession.createdAt,
358
+ sessionUpdatedAt: input.payload.agentSession.updatedAt,
359
+ action,
360
+ hostHandle: hostAgentHandle,
361
+ promptContext: input.payload.promptContext,
362
+ issueTitle: input.payload.agentSession.issue?.title,
363
+ issueDescription: input.payload.agentSession.issue?.description,
364
+ issueUrl: input.payload.agentSession.issue?.url,
365
+ issueTeamKey: extractIssueTeamKey(input.payload.agentSession.issue),
366
+ issueTeamName: extractIssueTeamName(input.payload.agentSession.issue),
367
+ issueTeamId: extractIssueTeamId(input.payload.agentSession.issue),
368
+ issueStateId: extractIssueStateField(input.payload.agentSession.issue, "id"),
369
+ issueStateName: extractIssueStateField(input.payload.agentSession.issue, "name"),
370
+ issueStateType: extractIssueStateField(input.payload.agentSession.issue, "type"),
371
+ issueAssigneeId: extractIssueAssigneeField(input.payload.agentSession.issue, "id"),
372
+ issueAssigneeName: extractIssueAssigneeField(input.payload.agentSession.issue, "displayName") ?? extractIssueAssigneeField(input.payload.agentSession.issue, "name"),
373
+ commentBody: input.payload.agentSession.comment?.body,
374
+ commentId: input.payload.agentSession.comment?.id,
375
+ sessionIntent,
376
+ suggestedPeerHandles,
377
+ webhookId: input.payload.webhookId,
378
+ webhookTimestamp: input.payload.webhookTimestamp,
379
+ oauthClientId: input.payload.oauthClientId,
380
+ organizationId: input.payload.organizationId,
381
+ appUserId: input.payload.appUserId,
382
+ writebackMode: config.writebackMode
383
+ });
384
+ const messageMetadata = {
385
+ linear_event_action: action,
386
+ linear_session_id: sessionId,
387
+ linear_issue_id: issueId,
388
+ linear_prompt_context: input.payload.promptContext ?? null,
389
+ linear_writeback_mode: config.writebackMode,
390
+ linear_bridge: "thenvoi",
391
+ linear_host_handle: hostAgentHandle
392
+ };
393
+ const shouldResetRoomSession = Boolean(
394
+ existingByIssue && existingByIssue.linearSessionId !== sessionId && existingByIssue.thenvoiRoomId === roomRecord.thenvoiRoomId && (existingByIssue.status === "completed" || existingByIssue.status === "errored")
395
+ );
396
+ const authenticatedHostHandle = runtime.authenticatedHostHandleCache.get(input.deps.thenvoiRest) ?? null;
397
+ const canBootstrapDirectly = options?.skipRoomWrite === true || authenticatedHostHandle === null || authenticatedHostHandle === hostAgentHandle;
398
+ if (!options?.skipRoomWrite || !canBootstrapDirectly) {
399
+ roomRecord = await forwardBridgeMessage({
400
+ thenvoiRest: input.deps.thenvoiRest,
401
+ store: input.deps.store,
402
+ logger,
403
+ roomRecord,
404
+ hostAgentHandle,
405
+ sessionId,
406
+ issueId,
407
+ message,
408
+ messageType: "task",
409
+ metadata: messageMetadata,
410
+ recoveredRoomRetryBaseDelayMs: input.config.recoveredRoomRetryBaseDelayMs
411
+ });
412
+ }
413
+ if (canBootstrapDirectly) {
414
+ const linearActor = resolveLinearActor(input.payload);
415
+ await enqueueBootstrapRequest(input.deps.store, {
416
+ eventKey,
417
+ linearSessionId: sessionId,
418
+ thenvoiRoomId: roomRecord.thenvoiRoomId,
419
+ expectedContent: message,
420
+ messageType: "task",
421
+ senderId: linearActor.id,
422
+ senderName: linearActor.name,
423
+ metadata: shouldResetRoomSession ? {
424
+ ...messageMetadata,
425
+ linear_reset_room_session: true
426
+ } : messageMetadata
427
+ });
428
+ }
429
+ await saveSessionRecord(input.deps.store, {
430
+ ...roomRecord,
431
+ status: "active",
432
+ lastEventKey: eventKey,
433
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
434
+ });
435
+ logger.info("linear_thenvoi_bridge.message_forwarded", {
436
+ sessionId,
437
+ issueId,
438
+ roomId: roomRecord.thenvoiRoomId,
439
+ action
440
+ });
441
+ } catch (error) {
442
+ try {
443
+ await postError(
444
+ input.deps.linearClient,
445
+ sessionId,
446
+ `Bridge error: ${error instanceof Error ? error.message : String(error)}`
447
+ );
448
+ } catch (reportError) {
449
+ logger.warn("linear_thenvoi_bridge.error_reporting_failed", {
450
+ sessionId,
451
+ error: reportError instanceof Error ? reportError.message : String(reportError)
452
+ });
453
+ }
454
+ if (roomRecord) {
455
+ await saveSessionRecord(input.deps.store, {
456
+ ...roomRecord,
457
+ status: "errored",
458
+ lastEventKey: eventKey,
459
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
460
+ });
461
+ } else if (existingBySession) {
462
+ await saveSessionRecord(input.deps.store, {
463
+ ...existingBySession,
464
+ status: "errored",
465
+ lastEventKey: eventKey,
466
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
467
+ });
468
+ }
469
+ throw error;
470
+ }
471
+ }
472
+ async function completeLinearSession(input) {
473
+ await input.linearClient.createAgentActivity({
474
+ agentSessionId: input.agentSessionId,
475
+ content: {
476
+ type: L2.AgentActivityType.Response,
477
+ body: input.body
478
+ }
479
+ });
480
+ if (!input.store) {
481
+ return;
482
+ }
483
+ const existing = await input.store.getBySessionId(input.agentSessionId);
484
+ if (!existing) {
485
+ return;
486
+ }
487
+ await saveSessionRecord(input.store, {
488
+ ...existing,
489
+ status: "completed",
490
+ lastEventKey: input.lastEventKey ?? existing.lastEventKey ?? null,
491
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
492
+ });
493
+ }
494
+ function normalizeConfig(config) {
495
+ return {
496
+ roomStrategy: config.roomStrategy ?? "issue",
497
+ writebackMode: config.writebackMode ?? "final_only",
498
+ hostAgentHandle: normalizeOptionalHandle(config.hostAgentHandle)
499
+ };
500
+ }
501
+ function normalizeOptionalHandle(handle) {
502
+ if (!handle) {
503
+ return null;
504
+ }
505
+ const normalized = stripHandlePrefix(handle);
506
+ return normalized.length > 0 ? normalized : null;
507
+ }
508
+ async function resolveHostAgentHandle(input) {
509
+ if (input.configuredHostHandle) {
510
+ input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, input.configuredHostHandle);
511
+ return input.configuredHostHandle;
512
+ }
513
+ const cachedAuthenticated = input.runtime.authenticatedHostHandleCache.get(input.thenvoiRest);
514
+ if (cachedAuthenticated) {
515
+ input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, cachedAuthenticated);
516
+ return cachedAuthenticated;
517
+ }
518
+ const cached = input.runtime.resolvedHostHandleCache.get(input.thenvoiRest);
519
+ if (cached) {
520
+ return cached;
521
+ }
522
+ const identity = await input.thenvoiRest.getAgentMe();
523
+ const authenticatedAgentHandle = normalizeOptionalHandle(identity.handle);
524
+ if (authenticatedAgentHandle) {
525
+ input.runtime.authenticatedHostHandleCache.set(input.thenvoiRest, authenticatedAgentHandle);
526
+ input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, authenticatedAgentHandle);
527
+ return authenticatedAgentHandle;
528
+ }
529
+ throw new Error(
530
+ "Linear bridge could not resolve the host agent handle. Set hostAgentHandle or use a REST adapter whose getAgentMe() returns handle."
531
+ );
532
+ }
533
+ function normalizeAction(action) {
534
+ if (!action) {
535
+ return null;
536
+ }
537
+ const normalized = action.trim().toLowerCase();
538
+ if (!SUPPORTED_ACTIONS.has(normalized)) {
539
+ return null;
540
+ }
541
+ return normalized;
542
+ }
543
+ async function handleCanceledAction(input) {
544
+ const existing = await input.deps.store.getBySessionId(input.sessionId);
545
+ if (!existing) {
546
+ input.logger.info("linear_thenvoi_bridge.session_canceled_without_room", {
547
+ sessionId: input.sessionId,
548
+ issueId: input.issueId,
549
+ action: input.payloadAction
550
+ });
551
+ return;
552
+ }
553
+ await saveSessionRecord(input.deps.store, {
554
+ ...existing,
555
+ status: "canceled",
556
+ lastEventKey: input.eventKey,
557
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
558
+ });
559
+ if (!input.skipRoomWrite) {
560
+ await input.deps.thenvoiRest.createChatEvent(existing.thenvoiRoomId, {
561
+ content: "[Linear]: Agent session canceled. Stop in-room execution and await new instructions.",
562
+ messageType: "task",
563
+ metadata: {
564
+ linear_event_action: "canceled",
565
+ linear_session_id: input.sessionId,
566
+ linear_issue_id: input.issueId,
567
+ linear_bridge: "thenvoi"
568
+ }
569
+ });
570
+ }
571
+ input.logger.info("linear_thenvoi_bridge.session_canceled", {
572
+ sessionId: input.sessionId,
573
+ issueId: input.issueId,
574
+ hadRoom: Boolean(existing),
575
+ action: input.payloadAction
576
+ });
577
+ }
578
+ async function handlePromptedAction(input) {
579
+ const existing = await input.deps.store.getBySessionId(input.sessionId);
580
+ if (!existing) {
581
+ input.logger.warn("linear_thenvoi_bridge.prompted_no_room", {
582
+ sessionId: input.sessionId
583
+ });
584
+ return;
585
+ }
586
+ const userResponse = extractPromptedResponseBody(input.payload);
587
+ if (!userResponse) {
588
+ input.logger.warn("linear_thenvoi_bridge.prompted_empty_response", {
589
+ sessionId: input.sessionId
590
+ });
591
+ return;
592
+ }
593
+ const message = `[Linear User Response]: ${userResponse}`;
594
+ const metadata = {
595
+ linear_event_action: "prompted",
596
+ linear_session_id: input.sessionId,
597
+ linear_bridge: "thenvoi"
598
+ };
599
+ const canBootstrapDirectly = input.config.hostAgentHandle !== null ? true : Boolean(normalizeOptionalHandle((await input.deps.thenvoiRest.getAgentMe()).handle));
600
+ if (!input.skipRoomWrite || !canBootstrapDirectly) {
601
+ await input.deps.thenvoiRest.createChatEvent(existing.thenvoiRoomId, {
602
+ content: message,
603
+ messageType: "text",
604
+ metadata
605
+ });
606
+ }
607
+ if (canBootstrapDirectly) {
608
+ const linearActor = resolveLinearActor(input.payload);
609
+ await enqueueBootstrapRequest(input.deps.store, {
610
+ eventKey: input.eventKey,
611
+ linearSessionId: input.sessionId,
612
+ thenvoiRoomId: existing.thenvoiRoomId,
613
+ expectedContent: message,
614
+ messageType: "text",
615
+ senderId: linearActor.id,
616
+ senderName: linearActor.name,
617
+ metadata
618
+ });
619
+ }
620
+ await saveSessionRecord(input.deps.store, {
621
+ ...existing,
622
+ status: "waiting",
623
+ lastEventKey: input.eventKey,
624
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
625
+ });
626
+ input.logger.info("linear_thenvoi_bridge.prompted_forwarded", {
627
+ sessionId: input.sessionId,
628
+ roomId: existing.thenvoiRoomId
629
+ });
630
+ }
631
+ async function enqueueBootstrapRequest(store, request) {
632
+ const now = /* @__PURE__ */ new Date();
633
+ await store.enqueueBootstrapRequest({
634
+ ...request,
635
+ createdAt: now.toISOString(),
636
+ expiresAt: new Date(now.getTime() + 10 * 6e4).toISOString()
637
+ });
638
+ }
639
+ function resolveLinearActor(payload) {
640
+ const creator = payload.agentSession.creator;
641
+ const creatorName = typeof creator?.name === "string" ? creator.name.trim() : "";
642
+ const creatorId = typeof creator?.id === "string" ? creator.id.trim() : "";
643
+ if (creatorName.length > 0) {
644
+ return {
645
+ id: creatorId.length > 0 ? creatorId : payload.agentSession.creatorId ?? payload.appUserId ?? null,
646
+ name: creatorName
647
+ };
648
+ }
649
+ return {
650
+ id: payload.agentSession.creatorId ?? payload.appUserId ?? null,
651
+ name: "Linear User"
652
+ };
653
+ }
654
+ async function resolveRoomRecord(input) {
655
+ const lockScope = input.roomStrategy === "issue" && input.issueId ? `issue:${input.issueId}` : `session:${input.sessionId}`;
656
+ const existing = input.runtime.roomResolutionLocks.get(lockScope);
657
+ if (existing) {
658
+ return existing;
659
+ }
660
+ const promise = resolveRoomRecordImpl(input);
661
+ input.runtime.roomResolutionLocks.set(lockScope, promise);
662
+ try {
663
+ return await promise;
664
+ } finally {
665
+ input.runtime.roomResolutionLocks.delete(lockScope);
666
+ }
667
+ }
668
+ async function forwardBridgeMessage(input) {
669
+ try {
670
+ await input.thenvoiRest.createChatEvent(input.roomRecord.thenvoiRoomId, {
671
+ content: input.message,
672
+ messageType: input.messageType,
673
+ metadata: input.metadata
674
+ });
675
+ return input.roomRecord;
676
+ } catch (error) {
677
+ if (!isRecoverableRoomAccessError(error)) {
678
+ throw error;
679
+ }
680
+ input.logger.warn("linear_thenvoi_bridge.room_forward_recovering_with_fresh_room", {
681
+ sessionId: input.sessionId,
682
+ issueId: input.issueId,
683
+ previousRoomId: input.roomRecord.thenvoiRoomId,
684
+ error: error instanceof Error ? error.message : String(error)
685
+ });
686
+ const recoveredRoom = await createFreshRoomRecord({
687
+ thenvoiRest: input.thenvoiRest,
688
+ store: input.store,
689
+ sessionId: input.sessionId,
690
+ issueId: input.issueId,
691
+ logger: input.logger
692
+ });
693
+ await ensureRoomParticipants({
694
+ thenvoiRest: input.thenvoiRest,
695
+ roomId: recoveredRoom.thenvoiRoomId,
696
+ handles: [input.hostAgentHandle],
697
+ logger: input.logger
698
+ });
699
+ let attempt = 0;
700
+ while (true) {
701
+ try {
702
+ await input.thenvoiRest.createChatEvent(recoveredRoom.thenvoiRoomId, {
703
+ content: input.message,
704
+ messageType: input.messageType,
705
+ metadata: input.metadata
706
+ });
707
+ return recoveredRoom;
708
+ } catch (recoveredError) {
709
+ if (!isRetryableRecoveredRoomEventError(recoveredError) || attempt >= RECOVERED_ROOM_EVENT_RETRY_LIMIT) {
710
+ throw recoveredError;
711
+ }
712
+ attempt += 1;
713
+ const retryBaseDelayMs = input.recoveredRoomRetryBaseDelayMs ?? RECOVERED_ROOM_EVENT_RETRY_BASE_DELAY_MS;
714
+ const delayMs = retryBaseDelayMs * attempt;
715
+ input.logger.warn("linear_thenvoi_bridge.room_recreated_retrying", {
716
+ sessionId: input.sessionId,
717
+ issueId: input.issueId,
718
+ roomId: recoveredRoom.thenvoiRoomId,
719
+ attempt,
720
+ delayMs,
721
+ error: recoveredError instanceof Error ? recoveredError.message : String(recoveredError)
722
+ });
723
+ await sleep(delayMs);
724
+ }
725
+ }
726
+ }
727
+ }
728
+ function isRecoverableRoomAccessError(error) {
729
+ if (!(error instanceof Error)) {
730
+ return false;
731
+ }
732
+ return /\/api\/v1\/agent\/chats\/.+\/events failed \((403|404)/.test(error.message);
733
+ }
734
+ function isRetryableRecoveredRoomEventError(error) {
735
+ if (!(error instanceof Error)) {
736
+ return false;
737
+ }
738
+ return /\/api\/v1\/agent\/chats\/.+\/events failed \((403|404|429)/.test(error.message);
739
+ }
740
+ function isRetryableRateLimitError(error) {
741
+ if (typeof error === "object" && error !== null && "retryable" in error) {
742
+ return error.retryable === true;
743
+ }
744
+ return error instanceof Error && /\b429\b/.test(error.message);
745
+ }
746
+ async function createFreshRoomRecord(input) {
747
+ const now = (/* @__PURE__ */ new Date()).toISOString();
748
+ const created = await input.thenvoiRest.createChat();
749
+ const record = {
750
+ linearSessionId: input.sessionId,
751
+ linearIssueId: input.issueId,
752
+ thenvoiRoomId: created.id,
753
+ status: "active",
754
+ lastEventKey: null,
755
+ createdAt: now,
756
+ updatedAt: now
757
+ };
758
+ await input.store.upsert(record);
759
+ input.logger.info("linear_thenvoi_bridge.room_recreated", {
760
+ sessionId: input.sessionId,
761
+ issueId: input.issueId,
762
+ roomId: created.id
763
+ });
764
+ return record;
765
+ }
766
+ async function resolveRoomRecordImpl(input) {
767
+ const existingBySession = await input.store.getBySessionId(input.sessionId);
768
+ if (existingBySession && existingBySession.status !== "canceled") {
769
+ return existingBySession;
770
+ }
771
+ if (input.roomStrategy === "issue" && input.issueId) {
772
+ const existingByIssue = await input.store.getByIssueId(input.issueId);
773
+ if (existingByIssue && existingByIssue.status !== "canceled") {
774
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
775
+ const linkedRecord = {
776
+ linearSessionId: input.sessionId,
777
+ linearIssueId: input.issueId,
778
+ thenvoiRoomId: existingByIssue.thenvoiRoomId,
779
+ status: "active",
780
+ lastEventKey: existingBySession?.lastEventKey ?? null,
781
+ createdAt: existingBySession?.createdAt ?? now2,
782
+ updatedAt: now2
783
+ };
784
+ await input.store.upsert(linkedRecord);
785
+ return linkedRecord;
786
+ }
787
+ }
788
+ const now = (/* @__PURE__ */ new Date()).toISOString();
789
+ const created = await input.thenvoiRest.createChat();
790
+ const createdRecord = {
791
+ linearSessionId: input.sessionId,
792
+ linearIssueId: input.issueId,
793
+ thenvoiRoomId: created.id,
794
+ status: "active",
795
+ lastEventKey: null,
796
+ createdAt: now,
797
+ updatedAt: now
798
+ };
799
+ await input.store.upsert(createdRecord);
800
+ input.logger.info("linear_thenvoi_bridge.room_created", {
801
+ sessionId: input.sessionId,
802
+ issueId: input.issueId,
803
+ roomId: created.id,
804
+ roomStrategy: input.roomStrategy
805
+ });
806
+ return createdRecord;
807
+ }
808
+ async function ensureRoomParticipants(input) {
809
+ if (input.handles.length === 0) {
810
+ return;
811
+ }
812
+ let existingParticipants;
813
+ try {
814
+ existingParticipants = await input.thenvoiRest.listChatParticipants(input.roomId);
815
+ } catch (error) {
816
+ if (isRetryableRateLimitError(error)) {
817
+ input.logger.warn("linear_thenvoi_bridge.participant_list_rate_limited", {
818
+ roomId: input.roomId,
819
+ error: error instanceof Error ? error.message : String(error)
820
+ });
821
+ return;
822
+ }
823
+ throw error;
824
+ }
825
+ const existingHandles = new Set(
826
+ existingParticipants.map((participant) => stripHandlePrefix(participant.handle ?? participant.name)).filter((value) => value.length > 0)
827
+ );
828
+ const missingHandles = input.handles.filter((handle) => !existingHandles.has(handle));
829
+ if (missingHandles.length === 0) {
830
+ return;
831
+ }
832
+ let peersByHandle;
833
+ try {
834
+ peersByHandle = await lookupPeerIdsByHandle(input.thenvoiRest, input.roomId, missingHandles);
835
+ } catch (error) {
836
+ if (error instanceof UnsupportedFeatureError) {
837
+ input.logger.warn("linear_thenvoi_bridge.peer_lookup_unavailable", {
838
+ roomId: input.roomId,
839
+ missingHandles,
840
+ reason: error.message
841
+ });
842
+ return;
843
+ }
844
+ throw error;
845
+ }
846
+ for (const handle of missingHandles) {
847
+ const participantId = peersByHandle.get(handle);
848
+ if (!participantId) {
849
+ input.logger.warn("linear_thenvoi_bridge.peer_not_found", {
850
+ roomId: input.roomId,
851
+ handle
852
+ });
853
+ continue;
854
+ }
855
+ try {
856
+ await input.thenvoiRest.addChatParticipant(
857
+ input.roomId,
858
+ {
859
+ participantId,
860
+ role: "member"
861
+ }
862
+ );
863
+ } catch (error) {
864
+ input.logger.warn("linear_thenvoi_bridge.add_participant_failed", {
865
+ roomId: input.roomId,
866
+ handle,
867
+ error: error instanceof Error ? error.message : String(error)
868
+ });
869
+ if (!isRetryableRateLimitError(error)) {
870
+ throw error;
871
+ }
872
+ }
873
+ }
874
+ }
875
+ async function lookupPeerIdsByHandle(thenvoiRest, roomId, handles) {
876
+ if (!thenvoiRest.listPeers) {
877
+ throw new UnsupportedFeatureError("Peer listing is not available in current REST adapter");
878
+ }
879
+ const targets = new Set(handles);
880
+ const found = /* @__PURE__ */ new Map();
881
+ for (let page = 1; page <= MAX_PEER_LOOKUP_PAGES && found.size < targets.size; page += 1) {
882
+ const response = await thenvoiRest.listPeers({
883
+ page,
884
+ pageSize: PEER_PAGE_SIZE,
885
+ notInChat: roomId
886
+ });
887
+ for (const peer of response.data) {
888
+ const peerId = peer.id;
889
+ if (typeof peerId !== "string" || peerId.length === 0) {
890
+ continue;
891
+ }
892
+ const handleCandidates = [
893
+ typeof peer.handle === "string" ? stripHandlePrefix(peer.handle) : "",
894
+ typeof peer.name === "string" ? stripHandlePrefix(peer.name) : ""
895
+ ].filter((value) => value.length > 0);
896
+ for (const candidate of handleCandidates) {
897
+ if (!targets.has(candidate)) {
898
+ continue;
899
+ }
900
+ found.set(candidate, peerId);
901
+ }
902
+ }
903
+ if (response.data.length < PEER_PAGE_SIZE) {
904
+ break;
905
+ }
906
+ }
907
+ return found;
908
+ }
909
+ async function selectRelevantPeerHandles(input) {
910
+ const configuredHandles = resolveConfiguredSpecialistHandles(input.intent, {
911
+ planningAgentHandles: input.planningAgentHandles,
912
+ implementationAgentHandles: input.implementationAgentHandles
913
+ }).filter((handle) => handle !== input.hostAgentHandle);
914
+ if (configuredHandles.length > 0) {
915
+ input.logger.info("linear_thenvoi_bridge.peer_prefetch_selected", {
916
+ roomId: input.roomId,
917
+ intent: input.intent,
918
+ handles: configuredHandles,
919
+ source: "configured"
920
+ });
921
+ return configuredHandles;
922
+ }
923
+ if (!input.thenvoiRest.listPeers) {
924
+ return [];
925
+ }
926
+ let peers;
927
+ try {
928
+ const response = await input.thenvoiRest.listPeers({
929
+ page: 1,
930
+ pageSize: PEER_PAGE_SIZE,
931
+ notInChat: input.roomId
932
+ });
933
+ peers = response.data;
934
+ } catch (error) {
935
+ if (error instanceof UnsupportedFeatureError) {
936
+ return [];
937
+ }
938
+ input.logger.warn("linear_thenvoi_bridge.peer_prefetch_failed", {
939
+ roomId: input.roomId,
940
+ intent: input.intent,
941
+ error: error instanceof Error ? error.message : String(error)
942
+ });
943
+ return [];
944
+ }
945
+ const ranked = peers.map((peer) => {
946
+ const handle = normalizeOptionalHandle(typeof peer.handle === "string" ? peer.handle : null);
947
+ return {
948
+ handle,
949
+ score: scorePeerForIntent(peer, input.intent)
950
+ };
951
+ }).filter(
952
+ (candidate) => typeof candidate.handle === "string" && candidate.handle.length > 0 && candidate.handle !== input.hostAgentHandle
953
+ ).filter((candidate) => candidate.score > 0).sort((left, right) => right.score - left.score);
954
+ const selected = ranked.slice(0, 2).map((candidate) => candidate.handle);
955
+ if (selected.length > 0) {
956
+ input.logger.info("linear_thenvoi_bridge.peer_prefetch_selected", {
957
+ roomId: input.roomId,
958
+ intent: input.intent,
959
+ handles: selected,
960
+ source: "lookup"
961
+ });
962
+ }
963
+ return selected;
964
+ }
965
+ function resolveConfiguredSpecialistHandles(intent, config) {
966
+ const handles = intent === "implementation" ? config.implementationAgentHandles : config.planningAgentHandles;
967
+ if (!handles || handles.length === 0) {
968
+ return [];
969
+ }
970
+ const configured = handles.map((value) => normalizeOptionalHandle(value)).filter((value) => Boolean(value));
971
+ if (intent === "implementation") {
972
+ return dedupeHandles(configured);
973
+ }
974
+ const ranked = configured.map((handle) => ({
975
+ handle,
976
+ score: /\bplanner\b/.test(handle) ? 3 : /\breviewer\b|\bcodex\b/.test(handle) ? 2 : 1
977
+ })).sort((left, right) => right.score - left.score).slice(0, 2).map((entry) => entry.handle);
978
+ return dedupeHandles(ranked);
979
+ }
980
+ function scorePeerForIntent(peer, intent) {
981
+ const description = typeof peer.description === "string" ? peer.description : "";
982
+ const haystack = [
983
+ typeof peer.name === "string" ? peer.name : "",
984
+ typeof peer.handle === "string" ? peer.handle : "",
985
+ description
986
+ ].join(" ").toLowerCase();
987
+ const weightedTerms = intent === "implementation" ? [
988
+ { pattern: /\bimplementer\b|\bimplementation\b/, score: 10 },
989
+ { pattern: /\bengineer\b|\bdeveloper\b|\bcoder\b|\bcoding\b/, score: 8 },
990
+ { pattern: /\bbuild\b|\bfrontend\b|\bui\b/, score: 5 },
991
+ { pattern: /\breviewer\b|\breview\b|\bplanner\b|\bplan\b/, score: -4 }
992
+ ] : [
993
+ { pattern: /\bplanner\b|\bplanning\b|\bplan\b/, score: 10 },
994
+ { pattern: /\bclaude\b/, score: 8 },
995
+ { pattern: /\breviewer\b|\breview\b|\bcodex\b/, score: 7 },
996
+ { pattern: /\borchestrator\b|\barchitect\b|\bdesign\b|\bscope\b|\bspec\b/, score: 6 },
997
+ { pattern: /\bimplementer\b|\bimplementation\b/, score: -3 }
998
+ ];
999
+ return weightedTerms.reduce((total, term) => term.pattern.test(haystack) ? total + term.score : total, 0);
1000
+ }
1001
+ async function sleep(ms) {
1002
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
1003
+ }
1004
+ async function saveSessionRecord(store, record) {
1005
+ await store.upsert(record);
1006
+ }
1007
+
1008
+ // src/integrations/linear/client.ts
1009
+ import { LinearClient } from "@linear/sdk";
1010
+ function isLinearApiKey(token) {
1011
+ return token.trim().startsWith("lin_api_");
1012
+ }
1013
+ function createLinearClient(token) {
1014
+ const trimmedToken = token.trim();
1015
+ if (isLinearApiKey(trimmedToken)) {
1016
+ return new LinearClient({
1017
+ apiKey: trimmedToken
1018
+ });
1019
+ }
1020
+ return new LinearClient({
1021
+ accessToken: trimmedToken
1022
+ });
1023
+ }
1024
+ function buildLinearAuthorizationHeader(token) {
1025
+ const trimmedToken = token.trim();
1026
+ if (isLinearApiKey(trimmedToken)) {
1027
+ return trimmedToken;
1028
+ }
1029
+ if (trimmedToken.startsWith("Bearer ")) {
1030
+ return trimmedToken;
1031
+ }
1032
+ return `Bearer ${trimmedToken}`;
1033
+ }
1034
+
1035
+ // src/integrations/linear/store.ts
1036
+ var SESSION_STATUSES = /* @__PURE__ */ new Set([
1037
+ "active",
1038
+ "waiting",
1039
+ "completed",
1040
+ "canceled",
1041
+ "errored"
1042
+ ]);
1043
+ var SqliteSessionRoomStore = class {
1044
+ dbPath;
1045
+ dbPromise = null;
1046
+ constructor(dbPath) {
1047
+ this.dbPath = dbPath;
1048
+ }
1049
+ async getBySessionId(sessionId) {
1050
+ const db = await this.getDb();
1051
+ const rawRow = db.prepare(
1052
+ `
1053
+ SELECT
1054
+ linear_session_id,
1055
+ linear_issue_id,
1056
+ thenvoi_room_id,
1057
+ status,
1058
+ last_event_key,
1059
+ created_at,
1060
+ updated_at
1061
+ FROM linear_thenvoi_session_rooms
1062
+ WHERE linear_session_id = ?
1063
+ LIMIT 1
1064
+ `
1065
+ ).get(sessionId);
1066
+ const row = parseSessionRoomRow(rawRow);
1067
+ return row ? this.toRecord(row) : null;
1068
+ }
1069
+ async getByIssueId(issueId) {
1070
+ const db = await this.getDb();
1071
+ const rawRow = db.prepare(
1072
+ `
1073
+ SELECT
1074
+ linear_session_id,
1075
+ linear_issue_id,
1076
+ thenvoi_room_id,
1077
+ status,
1078
+ last_event_key,
1079
+ created_at,
1080
+ updated_at
1081
+ FROM linear_thenvoi_session_rooms
1082
+ WHERE linear_issue_id = ? AND status != 'canceled'
1083
+ ORDER BY updated_at DESC
1084
+ LIMIT 1
1085
+ `
1086
+ ).get(issueId);
1087
+ const row = parseSessionRoomRow(rawRow);
1088
+ return row ? this.toRecord(row) : null;
1089
+ }
1090
+ async upsert(record) {
1091
+ const db = await this.getDb();
1092
+ db.prepare(
1093
+ `
1094
+ INSERT INTO linear_thenvoi_session_rooms (
1095
+ linear_session_id,
1096
+ linear_issue_id,
1097
+ thenvoi_room_id,
1098
+ status,
1099
+ last_event_key,
1100
+ created_at,
1101
+ updated_at
1102
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
1103
+ ON CONFLICT(linear_session_id)
1104
+ DO UPDATE SET
1105
+ linear_issue_id = excluded.linear_issue_id,
1106
+ thenvoi_room_id = excluded.thenvoi_room_id,
1107
+ status = excluded.status,
1108
+ last_event_key = excluded.last_event_key,
1109
+ updated_at = excluded.updated_at
1110
+ `
1111
+ ).run(
1112
+ record.linearSessionId,
1113
+ record.linearIssueId,
1114
+ record.thenvoiRoomId,
1115
+ record.status,
1116
+ record.lastEventKey ?? null,
1117
+ record.createdAt,
1118
+ record.updatedAt
1119
+ );
1120
+ }
1121
+ async markCanceled(sessionId) {
1122
+ const db = await this.getDb();
1123
+ db.prepare(
1124
+ `
1125
+ UPDATE linear_thenvoi_session_rooms
1126
+ SET status = 'canceled', updated_at = ?
1127
+ WHERE linear_session_id = ?
1128
+ `
1129
+ ).run((/* @__PURE__ */ new Date()).toISOString(), sessionId);
1130
+ }
1131
+ async enqueueBootstrapRequest(request) {
1132
+ const db = await this.getDb();
1133
+ db.prepare(
1134
+ `
1135
+ INSERT INTO linear_thenvoi_bootstrap_requests (
1136
+ event_key,
1137
+ linear_session_id,
1138
+ thenvoi_room_id,
1139
+ expected_content,
1140
+ message_type,
1141
+ metadata_json,
1142
+ created_at,
1143
+ expires_at,
1144
+ processed_at
1145
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)
1146
+ ON CONFLICT(event_key)
1147
+ DO UPDATE SET
1148
+ linear_session_id = excluded.linear_session_id,
1149
+ thenvoi_room_id = excluded.thenvoi_room_id,
1150
+ expected_content = excluded.expected_content,
1151
+ message_type = excluded.message_type,
1152
+ metadata_json = excluded.metadata_json,
1153
+ created_at = excluded.created_at,
1154
+ expires_at = excluded.expires_at,
1155
+ processed_at = NULL
1156
+ `
1157
+ ).run(
1158
+ request.eventKey,
1159
+ request.linearSessionId,
1160
+ request.thenvoiRoomId,
1161
+ request.expectedContent,
1162
+ request.messageType,
1163
+ request.metadata ? JSON.stringify(request.metadata) : null,
1164
+ request.createdAt,
1165
+ request.expiresAt
1166
+ );
1167
+ }
1168
+ async listPendingBootstrapRequests(limit = 20) {
1169
+ const db = await this.getDb();
1170
+ const rawRows = db.prepare(
1171
+ `
1172
+ SELECT
1173
+ event_key,
1174
+ linear_session_id,
1175
+ thenvoi_room_id,
1176
+ expected_content,
1177
+ message_type,
1178
+ metadata_json,
1179
+ created_at,
1180
+ expires_at,
1181
+ processed_at
1182
+ FROM linear_thenvoi_bootstrap_requests
1183
+ WHERE processed_at IS NULL AND expires_at > ?
1184
+ ORDER BY created_at ASC
1185
+ LIMIT ?
1186
+ `
1187
+ ).all((/* @__PURE__ */ new Date()).toISOString(), limit);
1188
+ const rows = parseBootstrapRequestRows(rawRows);
1189
+ return rows.map((row) => ({
1190
+ eventKey: row.event_key,
1191
+ linearSessionId: row.linear_session_id,
1192
+ thenvoiRoomId: row.thenvoi_room_id,
1193
+ expectedContent: row.expected_content,
1194
+ messageType: row.message_type,
1195
+ metadata: this.parseMetadata(row.metadata_json),
1196
+ createdAt: row.created_at,
1197
+ expiresAt: row.expires_at
1198
+ }));
1199
+ }
1200
+ async markBootstrapRequestProcessed(eventKey) {
1201
+ const db = await this.getDb();
1202
+ db.prepare(
1203
+ `
1204
+ UPDATE linear_thenvoi_bootstrap_requests
1205
+ SET processed_at = ?
1206
+ WHERE event_key = ? AND processed_at IS NULL
1207
+ `
1208
+ ).run((/* @__PURE__ */ new Date()).toISOString(), eventKey);
1209
+ }
1210
+ async close() {
1211
+ if (!this.dbPromise) {
1212
+ return;
1213
+ }
1214
+ const db = await this.dbPromise;
1215
+ db.close();
1216
+ this.dbPromise = null;
1217
+ }
1218
+ async getDb() {
1219
+ if (this.dbPromise) {
1220
+ return this.dbPromise;
1221
+ }
1222
+ this.dbPromise = this.initialize();
1223
+ return this.dbPromise;
1224
+ }
1225
+ async initialize() {
1226
+ const module = await import("sqlite").catch((error) => {
1227
+ throw new UnsupportedFeatureError(
1228
+ `SQLite store requires node:sqlite (Node.js 22+). Original error: ${error instanceof Error ? error.message : String(error)}`
1229
+ );
1230
+ });
1231
+ const db = new module.DatabaseSync(this.dbPath);
1232
+ db.exec(`
1233
+ CREATE TABLE IF NOT EXISTS linear_thenvoi_session_rooms (
1234
+ linear_session_id TEXT PRIMARY KEY,
1235
+ linear_issue_id TEXT,
1236
+ thenvoi_room_id TEXT NOT NULL,
1237
+ status TEXT NOT NULL,
1238
+ last_event_key TEXT,
1239
+ created_at TEXT NOT NULL,
1240
+ updated_at TEXT NOT NULL
1241
+ );
1242
+
1243
+ CREATE INDEX IF NOT EXISTS idx_linear_thenvoi_session_rooms_issue_active
1244
+ ON linear_thenvoi_session_rooms (linear_issue_id, status, updated_at);
1245
+
1246
+ CREATE TABLE IF NOT EXISTS linear_thenvoi_bootstrap_requests (
1247
+ event_key TEXT PRIMARY KEY,
1248
+ linear_session_id TEXT NOT NULL,
1249
+ thenvoi_room_id TEXT NOT NULL,
1250
+ expected_content TEXT NOT NULL,
1251
+ message_type TEXT NOT NULL,
1252
+ metadata_json TEXT,
1253
+ created_at TEXT NOT NULL,
1254
+ expires_at TEXT NOT NULL,
1255
+ processed_at TEXT
1256
+ );
1257
+
1258
+ CREATE INDEX IF NOT EXISTS idx_linear_thenvoi_bootstrap_requests_pending
1259
+ ON linear_thenvoi_bootstrap_requests (processed_at, expires_at, created_at);
1260
+ `);
1261
+ try {
1262
+ db.exec(`
1263
+ ALTER TABLE linear_thenvoi_session_rooms
1264
+ ADD COLUMN last_event_key TEXT
1265
+ `);
1266
+ } catch (error) {
1267
+ if (!isDuplicateColumnError(error)) {
1268
+ throw error;
1269
+ }
1270
+ }
1271
+ try {
1272
+ db.exec(`
1273
+ ALTER TABLE linear_thenvoi_bootstrap_requests
1274
+ ADD COLUMN message_type TEXT NOT NULL DEFAULT 'task'
1275
+ `);
1276
+ } catch (error) {
1277
+ if (!isDuplicateColumnError(error)) {
1278
+ throw error;
1279
+ }
1280
+ }
1281
+ try {
1282
+ db.exec(`
1283
+ ALTER TABLE linear_thenvoi_bootstrap_requests
1284
+ ADD COLUMN metadata_json TEXT
1285
+ `);
1286
+ } catch (error) {
1287
+ if (!isDuplicateColumnError(error)) {
1288
+ throw error;
1289
+ }
1290
+ }
1291
+ return db;
1292
+ }
1293
+ toRecord(row) {
1294
+ return {
1295
+ linearSessionId: row.linear_session_id,
1296
+ linearIssueId: row.linear_issue_id,
1297
+ thenvoiRoomId: row.thenvoi_room_id,
1298
+ status: row.status,
1299
+ lastEventKey: row.last_event_key,
1300
+ createdAt: row.created_at,
1301
+ updatedAt: row.updated_at
1302
+ };
1303
+ }
1304
+ parseMetadata(metadataJson) {
1305
+ if (!metadataJson) {
1306
+ return void 0;
1307
+ }
1308
+ try {
1309
+ const parsed = JSON.parse(metadataJson);
1310
+ return asRecord(parsed) ?? void 0;
1311
+ } catch (error) {
1312
+ const msg = error instanceof Error ? error.message : String(error);
1313
+ console.warn(`SqliteSessionRoomStore: failed to parse metadata_json: ${msg}`);
1314
+ return void 0;
1315
+ }
1316
+ }
1317
+ };
1318
+ function asRecord(value) {
1319
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1320
+ return null;
1321
+ }
1322
+ return value;
1323
+ }
1324
+ function isDuplicateColumnError(error) {
1325
+ if (!(error instanceof Error)) {
1326
+ return false;
1327
+ }
1328
+ return /duplicate column name/i.test(error.message);
1329
+ }
1330
+ function asString(value) {
1331
+ return typeof value === "string" ? value : null;
1332
+ }
1333
+ function asNullableString(value) {
1334
+ if (value === null) {
1335
+ return null;
1336
+ }
1337
+ if (value === void 0) {
1338
+ return void 0;
1339
+ }
1340
+ return asString(value);
1341
+ }
1342
+ function isSessionStatus(value) {
1343
+ return SESSION_STATUSES.has(value);
1344
+ }
1345
+ function parseSessionRoomRow(value) {
1346
+ const row = asRecord(value);
1347
+ if (!row) {
1348
+ return null;
1349
+ }
1350
+ const linearSessionId = asString(row.linear_session_id);
1351
+ const linearIssueId = asNullableString(row.linear_issue_id);
1352
+ const thenvoiRoomId = asString(row.thenvoi_room_id);
1353
+ const status = asString(row.status);
1354
+ const lastEventKey = asNullableString(row.last_event_key);
1355
+ const createdAt = asString(row.created_at);
1356
+ const updatedAt = asString(row.updated_at);
1357
+ if (!linearSessionId || linearIssueId === void 0 || !thenvoiRoomId || !status || !isSessionStatus(status) || lastEventKey === void 0 || !createdAt || !updatedAt) {
1358
+ return null;
1359
+ }
1360
+ return {
1361
+ linear_session_id: linearSessionId,
1362
+ linear_issue_id: linearIssueId,
1363
+ thenvoi_room_id: thenvoiRoomId,
1364
+ status,
1365
+ last_event_key: lastEventKey,
1366
+ created_at: createdAt,
1367
+ updated_at: updatedAt
1368
+ };
1369
+ }
1370
+ function parseBootstrapRequestRows(value) {
1371
+ if (!Array.isArray(value)) {
1372
+ return [];
1373
+ }
1374
+ return value.map((entry) => parseBootstrapRequestRow(entry)).filter((entry) => entry !== null);
1375
+ }
1376
+ function parseBootstrapRequestRow(value) {
1377
+ const row = asRecord(value);
1378
+ if (!row) {
1379
+ return null;
1380
+ }
1381
+ const eventKey = asString(row.event_key);
1382
+ const linearSessionId = asString(row.linear_session_id);
1383
+ const thenvoiRoomId = asString(row.thenvoi_room_id);
1384
+ const expectedContent = asString(row.expected_content);
1385
+ const messageType = asString(row.message_type);
1386
+ const metadataJson = asNullableString(row.metadata_json);
1387
+ const createdAt = asString(row.created_at);
1388
+ const expiresAt = asString(row.expires_at);
1389
+ const processedAt = asNullableString(row.processed_at);
1390
+ if (!eventKey || !linearSessionId || !thenvoiRoomId || !expectedContent || !messageType || metadataJson === void 0 || !createdAt || !expiresAt || processedAt === void 0) {
1391
+ return null;
1392
+ }
1393
+ return {
1394
+ event_key: eventKey,
1395
+ linear_session_id: linearSessionId,
1396
+ thenvoi_room_id: thenvoiRoomId,
1397
+ expected_content: expectedContent,
1398
+ message_type: messageType,
1399
+ metadata_json: metadataJson,
1400
+ created_at: createdAt,
1401
+ expires_at: expiresAt,
1402
+ processed_at: processedAt
1403
+ };
1404
+ }
1405
+ function createSqliteSessionRoomStore(dbPath) {
1406
+ return new SqliteSessionRoomStore(dbPath);
1407
+ }
1408
+
1409
+ // src/integrations/linear/tools.ts
1410
+ import { z } from "zod";
1411
+ function createLinearTools(options) {
1412
+ const { client, store, enableElicitation = true } = options;
1413
+ const sessionBodySchema = z.object({
1414
+ session_id: z.string().describe("The Linear agent session ID"),
1415
+ body: z.string().describe("The message body in Markdown format")
1416
+ });
1417
+ const issueIdInputSchema = z.object({
1418
+ issue_id: z.string().optional().describe("The Linear issue ID (UUID) from the session context")
1419
+ }).passthrough();
1420
+ const issueCommentLimitSchema = z.number().int().min(1).max(50).optional().describe("Maximum number of recent comments to return");
1421
+ const requiredIssueIdSchema = issueIdInputSchema;
1422
+ const optionalIssueIdSchema = issueIdInputSchema;
1423
+ const tools = [];
1424
+ const addSessionBodyTool = (name, description, handler) => {
1425
+ tools.push({
1426
+ name,
1427
+ description,
1428
+ schema: sessionBodySchema,
1429
+ handler
1430
+ });
1431
+ };
1432
+ addSessionBodyTool(
1433
+ "linear_post_thought",
1434
+ "Post a thought to the Linear agent session, visible to the user as internal reasoning.",
1435
+ async (args) => {
1436
+ await postThought(client, args.session_id, args.body);
1437
+ return { ok: true };
1438
+ }
1439
+ );
1440
+ addSessionBodyTool(
1441
+ "linear_post_action",
1442
+ "Post an action to the Linear agent session, showing the user what step is being taken.",
1443
+ async (args) => {
1444
+ await postAction(client, args.session_id, args.body);
1445
+ return { ok: true };
1446
+ }
1447
+ );
1448
+ addSessionBodyTool(
1449
+ "linear_post_error",
1450
+ "Post an error to the Linear agent session to notify the user of a failure.",
1451
+ async (args) => {
1452
+ await postError(client, args.session_id, args.body);
1453
+ return { ok: true };
1454
+ }
1455
+ );
1456
+ if (enableElicitation) {
1457
+ addSessionBodyTool(
1458
+ "linear_ask_user",
1459
+ "Ask the Linear user a question via an elicitation activity.",
1460
+ async (args) => {
1461
+ await postElicitation(client, args.session_id, args.body);
1462
+ return { ok: true };
1463
+ }
1464
+ );
1465
+ }
1466
+ addSessionBodyTool(
1467
+ "linear_post_response",
1468
+ "Post the final response to the Linear agent session and mark the session completed when a store is available.",
1469
+ async (args) => {
1470
+ await completeLinearSession({
1471
+ linearClient: client,
1472
+ agentSessionId: args.session_id,
1473
+ body: args.body,
1474
+ store
1475
+ });
1476
+ return { ok: true };
1477
+ }
1478
+ );
1479
+ tools.push(
1480
+ {
1481
+ name: "linear_update_plan",
1482
+ description: "Update the plan for the Linear agent session, showing progress on each step.",
1483
+ schema: z.object({
1484
+ session_id: z.string().describe("The Linear agent session ID"),
1485
+ steps: z.array(z.object({
1486
+ title: z.string().describe("Step title"),
1487
+ status: z.enum(["pending", "in_progress", "completed", "failed"]).describe("Step status")
1488
+ })).describe("The plan steps with their current status")
1489
+ }),
1490
+ handler: async (args) => {
1491
+ await updatePlan(
1492
+ client,
1493
+ args.session_id,
1494
+ args.steps
1495
+ );
1496
+ return { ok: true };
1497
+ }
1498
+ }
1499
+ );
1500
+ addIssueTools({
1501
+ tools,
1502
+ client,
1503
+ requiredIssueIdSchema,
1504
+ optionalIssueIdSchema,
1505
+ issueCommentLimitSchema
1506
+ });
1507
+ return tools;
1508
+ }
1509
+ function addIssueTools(input) {
1510
+ const {
1511
+ tools,
1512
+ client,
1513
+ requiredIssueIdSchema,
1514
+ optionalIssueIdSchema,
1515
+ issueCommentLimitSchema
1516
+ } = input;
1517
+ tools.push(
1518
+ {
1519
+ name: "linear_get_issue",
1520
+ description: "Fetch the current Linear issue details using the exact issue UUID from the session context.",
1521
+ schema: requiredIssueIdSchema,
1522
+ handler: async (args) => {
1523
+ const issueId = resolveIssueId("linear_get_issue", args);
1524
+ return readIssue(client, issueId);
1525
+ }
1526
+ },
1527
+ {
1528
+ name: "linear_list_issue_comments",
1529
+ description: "List recent comments for the current Linear issue using the exact issue UUID from the session context.",
1530
+ schema: requiredIssueIdSchema.extend({ limit: issueCommentLimitSchema }),
1531
+ handler: async (args) => {
1532
+ const issueId = resolveIssueId("linear_list_issue_comments", args);
1533
+ return readIssueComments(client, issueId, typeof args.limit === "number" ? args.limit : 20);
1534
+ }
1535
+ },
1536
+ {
1537
+ name: "linear_list_workflow_states",
1538
+ description: "List workflow states for the current issue's team so the bridge can move the issue between Todo, In Progress, In Review, and Done.",
1539
+ schema: optionalIssueIdSchema.extend({
1540
+ team_id: z.string().optional().describe("The Linear team ID when already known from the issue context")
1541
+ }),
1542
+ handler: async (args) => {
1543
+ const issueId = resolveOptionalIssueId("linear_list_workflow_states", args);
1544
+ const teamId = typeof args.team_id === "string" && args.team_id.trim().length > 0 ? args.team_id : await readIssueTeamId(client, issueId);
1545
+ if (!teamId) {
1546
+ throw new Error("linear_list_workflow_states requires issue_id or team_id.");
1547
+ }
1548
+ return readWorkflowStates(client, teamId);
1549
+ }
1550
+ },
1551
+ {
1552
+ name: "linear_update_issue",
1553
+ description: "Update a Linear issue (title/description/state/assignee/priority/etc.) from the session workflow.",
1554
+ schema: requiredIssueIdSchema.extend({
1555
+ title: z.string().optional().describe("New issue title"),
1556
+ description: z.string().optional().describe("New issue description (Markdown)"),
1557
+ priority: z.number().int().min(0).max(4).optional().describe("Priority 0-4"),
1558
+ state_id: z.string().optional().describe("Workflow state ID"),
1559
+ assignee_id: z.string().nullable().optional().describe("Assignee user ID, or null to unassign"),
1560
+ estimate: z.number().int().optional().describe("Estimate points"),
1561
+ due_date: z.string().optional().describe("Due date ISO string")
1562
+ }),
1563
+ handler: async (args) => {
1564
+ if (typeof client.updateIssue !== "function") {
1565
+ throw new Error("linear_update_issue is unavailable: Linear client does not support updateIssue().");
1566
+ }
1567
+ const issueId = resolveIssueId("linear_update_issue", args);
1568
+ const hasUpdates = args.title !== void 0 || args.description !== void 0 || args.priority !== void 0 || args.state_id !== void 0 || args.assignee_id !== void 0 || args.estimate !== void 0 || args.due_date !== void 0;
1569
+ if (!hasUpdates) {
1570
+ throw new Error("linear_update_issue requires at least one update field.");
1571
+ }
1572
+ await client.updateIssue(
1573
+ issueId,
1574
+ {
1575
+ ...args.title !== void 0 ? { title: args.title } : {},
1576
+ ...args.description !== void 0 ? { description: args.description } : {},
1577
+ ...args.priority !== void 0 ? { priority: args.priority } : {},
1578
+ ...args.state_id !== void 0 ? { stateId: args.state_id } : {},
1579
+ ...args.assignee_id !== void 0 ? { assigneeId: args.assignee_id } : {},
1580
+ ...args.estimate !== void 0 ? { estimate: args.estimate } : {},
1581
+ ...args.due_date !== void 0 ? { dueDate: args.due_date } : {}
1582
+ }
1583
+ );
1584
+ return { ok: true };
1585
+ }
1586
+ },
1587
+ {
1588
+ name: "linear_add_issue_comment",
1589
+ description: "Add a new comment to a Linear issue.",
1590
+ schema: requiredIssueIdSchema.extend({
1591
+ body: z.string().describe("Comment body (Markdown)")
1592
+ }),
1593
+ handler: async (args) => {
1594
+ if (typeof client.createComment !== "function") {
1595
+ throw new Error("linear_add_issue_comment is unavailable: Linear client does not support createComment().");
1596
+ }
1597
+ const issueId = resolveIssueId("linear_add_issue_comment", args);
1598
+ await client.createComment({
1599
+ issueId,
1600
+ body: args.body
1601
+ });
1602
+ return { ok: true };
1603
+ }
1604
+ }
1605
+ );
1606
+ }
1607
+ function assertUuid(toolName, value) {
1608
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1609
+ if (!uuidPattern.test(value)) {
1610
+ throw new Error(`${toolName} requires the exact Linear issue UUID from the session context. Received "${value}".`);
1611
+ }
1612
+ }
1613
+ function resolveIssueId(toolName, args) {
1614
+ const issueId = resolveOptionalIssueId(toolName, args);
1615
+ if (!issueId) {
1616
+ throw new Error(`${toolName} requires issue_id.`);
1617
+ }
1618
+ return issueId;
1619
+ }
1620
+ function resolveOptionalIssueId(toolName, args) {
1621
+ if (typeof args.issue_id !== "string" || args.issue_id.length === 0) {
1622
+ return void 0;
1623
+ }
1624
+ assertUuid(toolName, args.issue_id);
1625
+ return args.issue_id;
1626
+ }
1627
+ async function readIssue(client, issueId) {
1628
+ if (typeof client.issue !== "function") {
1629
+ throw new Error("linear_get_issue is unavailable: Linear client does not support issue().");
1630
+ }
1631
+ const issue = await client.issue(issueId);
1632
+ return {
1633
+ issue: {
1634
+ id: issue.id,
1635
+ identifier: issue.identifier ?? null,
1636
+ title: issue.title ?? null,
1637
+ description: issue.description ?? null,
1638
+ url: issue.url ?? null,
1639
+ priority: issue.priority ?? null,
1640
+ estimate: issue.estimate ?? null,
1641
+ due_date: issue.dueDate ?? null,
1642
+ created_at: issue.createdAt ?? null,
1643
+ updated_at: issue.updatedAt ?? null,
1644
+ state: issue.state ? {
1645
+ id: issue.state.id ?? null,
1646
+ name: issue.state.name ?? null,
1647
+ type: issue.state.type ?? null
1648
+ } : null,
1649
+ assignee: issue.assignee ? {
1650
+ id: issue.assignee.id ?? null,
1651
+ name: issue.assignee.displayName ?? issue.assignee.name ?? null
1652
+ } : null,
1653
+ team: issue.team ? {
1654
+ id: issue.team.id ?? null,
1655
+ key: issue.team.key ?? null,
1656
+ name: issue.team.name ?? null
1657
+ } : null
1658
+ }
1659
+ };
1660
+ }
1661
+ async function readIssueComments(client, issueId, limit) {
1662
+ if (typeof client.issue !== "function") {
1663
+ throw new Error("linear_list_issue_comments is unavailable: Linear client does not support issue().");
1664
+ }
1665
+ const issue = await client.issue(issueId);
1666
+ if (typeof issue.comments !== "function") {
1667
+ throw new Error("linear_list_issue_comments is unavailable: issue.comments() is not supported by the Linear client.");
1668
+ }
1669
+ const response = await issue.comments();
1670
+ const comments = (response.nodes ?? []).slice(-limit).map((comment) => ({
1671
+ id: comment.id ?? null,
1672
+ body: comment.body ?? null,
1673
+ created_at: comment.createdAt ?? null,
1674
+ updated_at: comment.updatedAt ?? null,
1675
+ user: comment.user ? {
1676
+ id: comment.user.id ?? null,
1677
+ name: comment.user.displayName ?? comment.user.name ?? null
1678
+ } : null
1679
+ }));
1680
+ return { comments };
1681
+ }
1682
+ async function readIssueTeamId(client, issueId) {
1683
+ if (!issueId) {
1684
+ return null;
1685
+ }
1686
+ if (typeof client.issue !== "function") {
1687
+ throw new Error("linear_list_workflow_states is unavailable: Linear client does not support issue().");
1688
+ }
1689
+ const issue = await client.issue(issueId);
1690
+ return issue.teamId ?? issue.team?.id ?? null;
1691
+ }
1692
+ async function readWorkflowStates(client, teamId) {
1693
+ if (typeof client.workflowStates !== "function") {
1694
+ throw new Error(
1695
+ "linear_list_workflow_states is unavailable: Linear client does not support workflowStates()."
1696
+ );
1697
+ }
1698
+ const response = await client.workflowStates({ teamId });
1699
+ const states = (response.nodes ?? []).map((state) => ({
1700
+ id: state.id ?? null,
1701
+ name: state.name ?? null,
1702
+ type: state.type ?? null,
1703
+ position: state.position ?? null,
1704
+ team_id: state.teamId ?? teamId
1705
+ })).sort((left, right) => (left.position ?? Number.MAX_SAFE_INTEGER) - (right.position ?? Number.MAX_SAFE_INTEGER));
1706
+ return { team_id: teamId, states };
1707
+ }
1708
+
1709
+ // src/integrations/linear/webhook.ts
1710
+ import {
1711
+ LinearWebhookClient,
1712
+ LINEAR_WEBHOOK_SIGNATURE_HEADER,
1713
+ LINEAR_WEBHOOK_TS_HEADER
1714
+ } from "@linear/sdk/webhooks";
1715
+ var DISPATCH_RETRY_LIMIT = 2;
1716
+ var DISPATCH_RETRY_BASE_DELAY_MS = 1e3;
1717
+ function createInlineLinearBridgeDispatcher(options) {
1718
+ const logger = options?.logger ?? new NoopLogger();
1719
+ const runtime = createLinearBridgeRuntime();
1720
+ return {
1721
+ dispatch: async (job) => {
1722
+ const result = await runDispatchAttempt(async () => {
1723
+ await handleAgentSessionEvent(job.input, {
1724
+ skipAcknowledgment: true,
1725
+ expectedEventKey: job.eventKey,
1726
+ runtime: job.runtime ?? runtime
1727
+ });
1728
+ }, {
1729
+ logger,
1730
+ failureEvent: "linear_thenvoi_bridge.dispatch_failed",
1731
+ retryEvent: "linear_thenvoi_bridge.dispatch_retrying",
1732
+ eventKey: job.eventKey,
1733
+ sessionId: job.input.payload.agentSession.id
1734
+ });
1735
+ if (result.ok) {
1736
+ await markBootstrapHandoffProcessed({
1737
+ eventKey: job.eventKey,
1738
+ store: job.input.deps.store,
1739
+ logger,
1740
+ sessionId: job.input.payload.agentSession.id
1741
+ });
1742
+ return;
1743
+ }
1744
+ const failure = {
1745
+ eventKey: job.eventKey,
1746
+ sessionId: job.input.payload.agentSession.id,
1747
+ attempts: result.attempts,
1748
+ error: result.error
1749
+ };
1750
+ await signalTerminalDispatchFailure({
1751
+ logger,
1752
+ job,
1753
+ failure
1754
+ });
1755
+ throw result.error;
1756
+ }
1757
+ };
1758
+ }
1759
+ function createInProcessLinearBridgeDispatcher(options) {
1760
+ const logger = options?.logger ?? new NoopLogger();
1761
+ const runtime = createLinearBridgeRuntime();
1762
+ const queued = /* @__PURE__ */ new Set();
1763
+ const inFlight = /* @__PURE__ */ new Set();
1764
+ const terminalFailures = [];
1765
+ return {
1766
+ isQueued: (eventKey) => queued.has(eventKey),
1767
+ waitForIdle: async () => {
1768
+ let snapshot = [...inFlight];
1769
+ while (snapshot.length > 0) {
1770
+ await Promise.allSettled(snapshot);
1771
+ snapshot = [...inFlight];
1772
+ }
1773
+ if (terminalFailures.length === 0) {
1774
+ return;
1775
+ }
1776
+ const failures = terminalFailures.splice(0, terminalFailures.length);
1777
+ throw new AggregateError(
1778
+ failures.map((failure) => toDispatchFailureError(failure)),
1779
+ `Linear bridge async dispatch failed for ${failures.length} event(s)`
1780
+ );
1781
+ },
1782
+ dispatch: async (job) => {
1783
+ if (queued.has(job.eventKey)) {
1784
+ return;
1785
+ }
1786
+ queued.add(job.eventKey);
1787
+ const dispatchTask = (async () => {
1788
+ await new Promise((resolveMicrotask) => {
1789
+ queueMicrotask(() => resolveMicrotask());
1790
+ });
1791
+ const result = await runDispatchAttempt(async () => {
1792
+ await handleAgentSessionEvent(job.input, {
1793
+ skipAcknowledgment: true,
1794
+ expectedEventKey: job.eventKey,
1795
+ runtime: job.runtime ?? runtime
1796
+ });
1797
+ }, {
1798
+ logger,
1799
+ failureEvent: "linear_thenvoi_bridge.async_dispatch_failed",
1800
+ retryEvent: "linear_thenvoi_bridge.async_dispatch_retrying",
1801
+ eventKey: job.eventKey,
1802
+ sessionId: job.input.payload.agentSession.id
1803
+ });
1804
+ if (result.ok) {
1805
+ await markBootstrapHandoffProcessed({
1806
+ eventKey: job.eventKey,
1807
+ store: job.input.deps.store,
1808
+ logger,
1809
+ sessionId: job.input.payload.agentSession.id
1810
+ });
1811
+ return;
1812
+ }
1813
+ const failure = {
1814
+ eventKey: job.eventKey,
1815
+ sessionId: job.input.payload.agentSession.id,
1816
+ attempts: result.attempts,
1817
+ error: result.error
1818
+ };
1819
+ terminalFailures.push(failure);
1820
+ await signalTerminalDispatchFailure({
1821
+ logger,
1822
+ job,
1823
+ failure
1824
+ });
1825
+ })();
1826
+ inFlight.add(dispatchTask);
1827
+ void dispatchTask.catch((error) => {
1828
+ logger.error("linear_thenvoi_bridge.async_dispatch_failure_signal_crashed", {
1829
+ eventKey: job.eventKey,
1830
+ sessionId: job.input.payload.agentSession.id,
1831
+ error: serializeError(error)
1832
+ });
1833
+ }).finally(() => {
1834
+ queued.delete(job.eventKey);
1835
+ inFlight.delete(dispatchTask);
1836
+ });
1837
+ await dispatchTask;
1838
+ }
1839
+ };
1840
+ }
1841
+ function createLinearWebhookHandler(options) {
1842
+ const logger = options.deps.logger ?? new NoopLogger();
1843
+ const runtime = createLinearBridgeRuntime();
1844
+ const dispatcher = options.dispatcher ?? createInlineLinearBridgeDispatcher({ logger });
1845
+ const webhookClient = new LinearWebhookClient(options.config.linearWebhookSecret);
1846
+ const inFlightEventKeys = /* @__PURE__ */ new Set();
1847
+ return async (request, response) => {
1848
+ if (request.method !== "POST") {
1849
+ logger.warn("linear_thenvoi_bridge.webhook_invalid_method", {
1850
+ method: request.method ?? null
1851
+ });
1852
+ sendText(response, 405, "Method not allowed");
1853
+ return;
1854
+ }
1855
+ const signature = getHeaderValue(request, LINEAR_WEBHOOK_SIGNATURE_HEADER);
1856
+ if (!signature) {
1857
+ logger.warn("linear_thenvoi_bridge.webhook_missing_signature", {});
1858
+ sendText(response, 400, "Missing webhook signature");
1859
+ return;
1860
+ }
1861
+ const timestamp = getHeaderValue(request, LINEAR_WEBHOOK_TS_HEADER);
1862
+ if (!timestamp) {
1863
+ logger.warn("linear_thenvoi_bridge.webhook_missing_timestamp", {});
1864
+ sendText(response, 400, "Missing webhook timestamp");
1865
+ return;
1866
+ }
1867
+ const rawBody = await readRawBody(request);
1868
+ let payload;
1869
+ try {
1870
+ payload = webhookClient.parseData(
1871
+ rawBody,
1872
+ signature,
1873
+ timestamp
1874
+ );
1875
+ } catch (error) {
1876
+ logger.warn("linear_thenvoi_bridge.webhook_invalid_signature", {
1877
+ error: error instanceof Error ? error.message : String(error)
1878
+ });
1879
+ sendText(response, 400, "Invalid webhook");
1880
+ return;
1881
+ }
1882
+ if (payload.type !== "AgentSessionEvent") {
1883
+ logger.info("linear_thenvoi_bridge.webhook_ignored_event", {
1884
+ type: payload.type
1885
+ });
1886
+ sendText(response, 200, "OK");
1887
+ return;
1888
+ }
1889
+ const eventKey = getAgentSessionEventKey(payload);
1890
+ logger.info("linear_thenvoi_bridge.webhook_received", {
1891
+ sessionId: payload.agentSession.id,
1892
+ issueId: payload.agentSession.issue?.id ?? null,
1893
+ action: payload.action,
1894
+ eventKey
1895
+ });
1896
+ const existing = await options.deps.store.getBySessionId(payload.agentSession.id);
1897
+ const alreadyInFlight = inFlightEventKeys.has(eventKey);
1898
+ if (existing?.lastEventKey === eventKey || alreadyInFlight || dispatcher.isQueued?.(eventKey)) {
1899
+ logger.info("linear_thenvoi_bridge.webhook_duplicate_ignored", {
1900
+ sessionId: payload.agentSession.id,
1901
+ eventKey
1902
+ });
1903
+ sendText(response, 200, "OK");
1904
+ return;
1905
+ }
1906
+ inFlightEventKeys.add(eventKey);
1907
+ try {
1908
+ if (normalizeAction2(payload.action) === "created") {
1909
+ try {
1910
+ await postThought(
1911
+ options.deps.linearClient,
1912
+ payload.agentSession.id,
1913
+ "Received session. Setting up workspace..."
1914
+ );
1915
+ } catch (error) {
1916
+ logger.warn("linear_thenvoi_bridge.webhook_acknowledgment_failed", {
1917
+ sessionId: payload.agentSession.id,
1918
+ error
1919
+ });
1920
+ }
1921
+ }
1922
+ await dispatcher.dispatch({
1923
+ eventKey,
1924
+ runtime,
1925
+ input: {
1926
+ payload,
1927
+ config: options.config,
1928
+ deps: options.deps
1929
+ }
1930
+ });
1931
+ } catch (error) {
1932
+ logger.error("linear_thenvoi_bridge.webhook_dispatch_failed", {
1933
+ sessionId: payload.agentSession.id,
1934
+ issueId: payload.agentSession.issue?.id ?? null,
1935
+ action: payload.action,
1936
+ eventKey,
1937
+ error: serializeError(error)
1938
+ });
1939
+ sendText(response, isRetryableDispatchError(error) ? 503 : 500, "Dispatch failed");
1940
+ return;
1941
+ } finally {
1942
+ inFlightEventKeys.delete(eventKey);
1943
+ }
1944
+ logger.info("linear_thenvoi_bridge.webhook_dispatched", {
1945
+ sessionId: payload.agentSession.id,
1946
+ issueId: payload.agentSession.issue?.id ?? null,
1947
+ action: payload.action,
1948
+ eventKey
1949
+ });
1950
+ sendText(response, 200, "OK");
1951
+ };
1952
+ }
1953
+ function normalizeAction2(action) {
1954
+ return typeof action === "string" ? action.trim().toLowerCase() : "";
1955
+ }
1956
+ function getHeaderValue(request, name) {
1957
+ const value = request.headers[name];
1958
+ if (Array.isArray(value)) {
1959
+ return value[0] ?? null;
1960
+ }
1961
+ return typeof value === "string" ? value : null;
1962
+ }
1963
+ async function readRawBody(request) {
1964
+ if (Buffer.isBuffer(request.body)) {
1965
+ return request.body;
1966
+ }
1967
+ if (typeof request.body === "string") {
1968
+ return Buffer.from(request.body);
1969
+ }
1970
+ if (request.body instanceof Uint8Array) {
1971
+ return Buffer.from(request.body);
1972
+ }
1973
+ const chunks = [];
1974
+ for await (const chunk of request) {
1975
+ if (typeof chunk === "string") {
1976
+ chunks.push(Buffer.from(chunk));
1977
+ } else {
1978
+ chunks.push(Buffer.from(chunk));
1979
+ }
1980
+ }
1981
+ return Buffer.concat(chunks);
1982
+ }
1983
+ function sendText(response, statusCode, body) {
1984
+ if (response.headersSent) {
1985
+ return;
1986
+ }
1987
+ response.statusCode = statusCode;
1988
+ response.end(body);
1989
+ }
1990
+ async function runDispatchAttempt(task, context) {
1991
+ const retryLimit = context.retryLimit ?? DISPATCH_RETRY_LIMIT;
1992
+ const retryBaseDelayMs = context.retryBaseDelayMs ?? DISPATCH_RETRY_BASE_DELAY_MS;
1993
+ let attempt = 0;
1994
+ while (true) {
1995
+ try {
1996
+ await task();
1997
+ return { ok: true };
1998
+ } catch (error) {
1999
+ if (!isRetryableDispatchError(error) || attempt >= retryLimit) {
2000
+ context.logger.error(context.failureEvent, {
2001
+ eventKey: context.eventKey,
2002
+ sessionId: context.sessionId,
2003
+ error: serializeError(error)
2004
+ });
2005
+ return {
2006
+ ok: false,
2007
+ attempts: attempt + 1,
2008
+ error
2009
+ };
2010
+ }
2011
+ attempt += 1;
2012
+ context.logger.warn(context.retryEvent, {
2013
+ eventKey: context.eventKey,
2014
+ sessionId: context.sessionId,
2015
+ attempt,
2016
+ delayMs: retryBaseDelayMs * attempt,
2017
+ error: serializeError(error)
2018
+ });
2019
+ await sleep2(retryBaseDelayMs * attempt);
2020
+ }
2021
+ }
2022
+ }
2023
+ async function markBootstrapHandoffProcessed(input) {
2024
+ try {
2025
+ await input.store.markBootstrapRequestProcessed(input.eventKey);
2026
+ } catch (error) {
2027
+ input.logger.warn("linear_thenvoi_bridge.bootstrap_handoff_mark_processed_failed", {
2028
+ eventKey: input.eventKey,
2029
+ sessionId: input.sessionId,
2030
+ error: serializeError(error)
2031
+ });
2032
+ }
2033
+ }
2034
+ async function signalTerminalDispatchFailure(input) {
2035
+ const { logger, job, failure } = input;
2036
+ try {
2037
+ const existing = await job.input.deps.store.getBySessionId(failure.sessionId);
2038
+ if (existing) {
2039
+ await job.input.deps.store.upsert({
2040
+ ...existing,
2041
+ status: "errored",
2042
+ lastEventKey: job.eventKey,
2043
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2044
+ });
2045
+ logger.warn("linear_thenvoi_bridge.async_dispatch_terminal_failure_signaled", {
2046
+ eventKey: failure.eventKey,
2047
+ sessionId: failure.sessionId,
2048
+ attempts: failure.attempts,
2049
+ signal: "session_store_status_errored"
2050
+ });
2051
+ return;
2052
+ }
2053
+ } catch (storeError) {
2054
+ logger.error("linear_thenvoi_bridge.async_dispatch_terminal_failure_store_signal_failed", {
2055
+ eventKey: failure.eventKey,
2056
+ sessionId: failure.sessionId,
2057
+ attempts: failure.attempts,
2058
+ dispatchError: serializeError(failure.error),
2059
+ signalError: serializeError(storeError)
2060
+ });
2061
+ }
2062
+ try {
2063
+ await postError(
2064
+ job.input.deps.linearClient,
2065
+ failure.sessionId,
2066
+ "Bridge dispatch failed and could not recover automatically. Retry this session event in Linear."
2067
+ );
2068
+ logger.warn("linear_thenvoi_bridge.async_dispatch_terminal_failure_signaled", {
2069
+ eventKey: failure.eventKey,
2070
+ sessionId: failure.sessionId,
2071
+ attempts: failure.attempts,
2072
+ signal: "linear_activity_error"
2073
+ });
2074
+ } catch (signalError) {
2075
+ logger.error("linear_thenvoi_bridge.async_dispatch_terminal_failure_signal_failed", {
2076
+ eventKey: failure.eventKey,
2077
+ sessionId: failure.sessionId,
2078
+ attempts: failure.attempts,
2079
+ dispatchError: serializeError(failure.error),
2080
+ signalError: serializeError(signalError)
2081
+ });
2082
+ }
2083
+ }
2084
+ function isRetryableDispatchError(error) {
2085
+ return typeof error === "object" && error !== null && "retryable" in error && error.retryable === true;
2086
+ }
2087
+ async function sleep2(ms) {
2088
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
2089
+ }
2090
+ function serializeError(error) {
2091
+ if (error instanceof Error) {
2092
+ return {
2093
+ name: error.name,
2094
+ message: error.message,
2095
+ stack: error.stack,
2096
+ ...typeof error.retryable !== "undefined" ? { retryable: error.retryable } : {}
2097
+ };
2098
+ }
2099
+ return {
2100
+ message: String(error)
2101
+ };
2102
+ }
2103
+ function toDispatchFailureError(failure) {
2104
+ return new Error(
2105
+ `Linear bridge async dispatch failed for event ${failure.eventKey} (session ${failure.sessionId})`,
2106
+ { cause: failure.error }
2107
+ );
2108
+ }
2109
+
2110
+ // src/integrations/linear/constants.ts
2111
+ var DEFAULT_STATUS_MAPPING = {
2112
+ active: "active",
2113
+ waiting: "waiting",
2114
+ canceled: "canceled",
2115
+ completed: "completed",
2116
+ errored: "errored"
2117
+ };
2118
+ export {
2119
+ DEFAULT_STATUS_MAPPING,
2120
+ buildLinearAuthorizationHeader,
2121
+ completeLinearSession,
2122
+ createInProcessLinearBridgeDispatcher,
2123
+ createInlineLinearBridgeDispatcher,
2124
+ createLinearBridgeRuntime,
2125
+ createLinearClient,
2126
+ createLinearTools,
2127
+ createLinearWebhookHandler,
2128
+ createSqliteSessionRoomStore,
2129
+ dedupeHandles,
2130
+ handleAgentSessionEvent,
2131
+ isLinearApiKey,
2132
+ postAction,
2133
+ postElicitation,
2134
+ postError,
2135
+ postResponse,
2136
+ postThought,
2137
+ stripHandlePrefix,
2138
+ updatePlan
2139
+ };