@band-ai/sdk 0.1.4 → 0.1.10
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.
- package/README.md +372 -0
- package/dist/{ClaudeSDKAdapter-CRq4R-WB.d.cts → ClaudeSDKAdapter-DfjaI5ke.d.cts} +9 -7
- package/dist/{ClaudeSDKAdapter-BHOkjN3X.d.ts → ClaudeSDKAdapter-DonPGJ43.d.ts} +9 -7
- package/dist/{acp-server-BdRz9_UN.d.cts → acp-server-CUBXIiG6.d.cts} +2 -2
- package/dist/{acp-server-B1Da7L6u.d.ts → acp-server-NZAeg-WO.d.ts} +2 -2
- package/dist/adapters.cjs +494 -106
- package/dist/adapters.d.cts +12 -12
- package/dist/adapters.d.ts +12 -12
- package/dist/adapters.js +7 -7
- package/dist/{backends-SVMT-7l4.d.cts → backends-DYtEhCZG.d.cts} +7 -2
- package/dist/{backends-CzDbiQar.d.ts → backends-oYAb7EU-.d.ts} +7 -2
- package/dist/{chunk-MSL6CWHY.js → chunk-2BHHR6YW.js} +17 -16
- package/dist/{chunk-EDPKUUJL.js → chunk-6RXWL6TC.js} +9 -1
- package/dist/{chunk-SFVKOEQH.js → chunk-DNPVCR3S.js} +92 -14
- package/dist/{chunk-I5EZG52P.js → chunk-FP237EAG.js} +424 -136
- package/dist/{chunk-RHVH2GZR.js → chunk-J3M3PFFY.js} +81 -31
- package/dist/{chunk-GVEQ3RAH.js → chunk-MLROFDKR.js} +132 -23
- package/dist/{chunk-INA6YZTZ.js → chunk-RDM4TE4B.js} +32 -30
- package/dist/chunk-US226AS4.js +130 -0
- package/dist/{chunk-W5NDZSYE.js → chunk-XA4N2JPH.js} +1 -1
- package/dist/converters.cjs +469 -13
- package/dist/converters.d.cts +5 -5
- package/dist/converters.d.ts +5 -5
- package/dist/converters.js +3 -2
- package/dist/core.cjs +45 -2
- package/dist/core.d.cts +4 -3
- package/dist/core.d.ts +4 -3
- package/dist/core.js +5 -1
- package/dist/disconnectReason-IuADl6TH.d.cts +34 -0
- package/dist/disconnectReason-IuADl6TH.d.ts +34 -0
- package/dist/{dtos-DUpbIu8k.d.cts → dtos-Dd_fTfLn.d.cts} +26 -6
- package/dist/{dtos-DUpbIu8k.d.ts → dtos-Dd_fTfLn.d.ts} +26 -6
- package/dist/index.cjs +1089 -269
- package/dist/index.d.cts +16 -15
- package/dist/index.d.ts +16 -15
- package/dist/index.js +14 -8
- package/dist/linear.cjs +985 -53
- package/dist/linear.d.cts +137 -7
- package/dist/linear.d.ts +137 -7
- package/dist/linear.js +974 -53
- package/dist/mcp-claude.cjs +303 -32
- package/dist/mcp-claude.d.cts +3 -3
- package/dist/mcp-claude.d.ts +3 -3
- package/dist/mcp-claude.js +210 -12
- package/dist/mcp.cjs +313 -45
- package/dist/mcp.d.cts +5 -5
- package/dist/mcp.d.ts +5 -5
- package/dist/mcp.js +3 -3
- package/dist/{opencode-BBcDchcN.d.cts → opencode--iA5-KYk.d.cts} +2 -2
- package/dist/{opencode-DthQQUsZ.d.ts → opencode-YNacv-eJ.d.ts} +2 -2
- package/dist/{pagination-CcDkVFxK.d.ts → pagination-BBLsYLhT.d.ts} +2 -2
- package/dist/{pagination-BPiys10t.d.cts → pagination-CqidD-L_.d.cts} +2 -2
- package/dist/{protocols-DrhzOH6K.d.ts → protocols-BF6jamIl.d.ts} +1 -1
- package/dist/{protocols-CInhtFpA.d.cts → protocols-D2bTrQ_t.d.cts} +1 -1
- package/dist/rest.d.cts +5 -5
- package/dist/rest.d.ts +5 -5
- package/dist/runtime.cjs +721 -172
- package/dist/runtime.d.cts +15 -10
- package/dist/runtime.d.ts +15 -10
- package/dist/runtime.js +11 -8
- package/dist/{schemas-vos1AaBc.d.ts → schemas-B1Fd3oUM.d.ts} +17 -8
- package/dist/{schemas-BpEBkq5V.d.cts → schemas-CI0qgcp_.d.cts} +17 -8
- package/dist/{sdk-CcnQ5OUC.d.ts → sdk-CyqN1W5g.d.ts} +32 -2
- package/dist/{sdk-CwTAxhwI.d.cts → sdk-D2rTDU_o.d.cts} +32 -2
- package/dist/{simpleAdapter-DqhvjsGr.d.cts → simpleAdapter-C_f-01Cu.d.cts} +1 -1
- package/dist/{simpleAdapter-9OE_p6QW.d.ts → simpleAdapter-qQt-tSz4.d.ts} +1 -1
- package/dist/testing.d.cts +3 -3
- package/dist/testing.d.ts +3 -3
- package/dist/{types-CovcHpYf.d.cts → types-CKqrhGB1.d.cts} +2 -2
- package/dist/{types-CxII3XkA.d.ts → types-D3AsUnd0.d.ts} +1 -1
- package/dist/{types-7jVzOeOP.d.ts → types-DNySTI_8.d.ts} +2 -2
- package/dist/{types-CSHFipIc.d.cts → types-DtMms5Fi.d.cts} +1 -1
- package/package.json +22 -21
package/dist/linear.cjs
CHANGED
|
@@ -31,6 +31,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var linear_exports = {};
|
|
32
32
|
__export(linear_exports, {
|
|
33
33
|
DEFAULT_STATUS_MAPPING: () => DEFAULT_STATUS_MAPPING,
|
|
34
|
+
ELICITATION_BODY_MAX_LENGTH: () => ELICITATION_BODY_MAX_LENGTH,
|
|
35
|
+
PROVIDER_MAX_LENGTH: () => PROVIDER_MAX_LENGTH,
|
|
36
|
+
SELECT_OPTION_MAX_LENGTH: () => SELECT_OPTION_MAX_LENGTH,
|
|
37
|
+
STALE_SESSION_CHECK_INTERVAL_MS: () => STALE_SESSION_CHECK_INTERVAL_MS,
|
|
38
|
+
STALE_SESSION_THRESHOLD_MS: () => STALE_SESSION_THRESHOLD_MS,
|
|
39
|
+
StaleSessionGuard: () => StaleSessionGuard,
|
|
34
40
|
buildLinearAuthorizationHeader: () => buildLinearAuthorizationHeader,
|
|
35
41
|
completeLinearSession: () => completeLinearSession,
|
|
36
42
|
createInProcessLinearBridgeDispatcher: () => createInProcessLinearBridgeDispatcher,
|
|
@@ -42,12 +48,17 @@ __export(linear_exports, {
|
|
|
42
48
|
createSqliteSessionRoomStore: () => createSqliteSessionRoomStore,
|
|
43
49
|
dedupeHandles: () => dedupeHandles,
|
|
44
50
|
handleAgentSessionEvent: () => handleAgentSessionEvent,
|
|
51
|
+
handleAppUserNotification: () => handleAppUserNotification,
|
|
45
52
|
isLinearApiKey: () => isLinearApiKey,
|
|
53
|
+
isSessionStale: () => isSessionStale,
|
|
46
54
|
postAction: () => postAction,
|
|
55
|
+
postAuthElicitation: () => postAuthElicitation,
|
|
47
56
|
postElicitation: () => postElicitation,
|
|
48
57
|
postError: () => postError,
|
|
49
58
|
postResponse: () => postResponse,
|
|
59
|
+
postSelectElicitation: () => postSelectElicitation,
|
|
50
60
|
postThought: () => postThought,
|
|
61
|
+
sendRecoveryActivityIfStale: () => sendRecoveryActivityIfStale,
|
|
51
62
|
stripHandlePrefix: () => stripHandlePrefix,
|
|
52
63
|
updatePlan: () => updatePlan
|
|
53
64
|
});
|
|
@@ -99,20 +110,24 @@ function dedupeHandles(handles) {
|
|
|
99
110
|
|
|
100
111
|
// src/integrations/linear/activities.ts
|
|
101
112
|
var import_sdk = require("@linear/sdk");
|
|
102
|
-
|
|
113
|
+
var ELICITATION_BODY_MAX_LENGTH = 1e4;
|
|
114
|
+
var SELECT_OPTION_MAX_LENGTH = 200;
|
|
115
|
+
var PROVIDER_MAX_LENGTH = 100;
|
|
116
|
+
async function postActivity(client, sessionId, content, options) {
|
|
103
117
|
await client.createAgentActivity({
|
|
104
118
|
agentSessionId: sessionId,
|
|
105
|
-
content
|
|
119
|
+
content,
|
|
120
|
+
...options?.ephemeral ? { ephemeral: true } : {}
|
|
106
121
|
});
|
|
107
122
|
}
|
|
108
|
-
async function postBodyActivity(client, sessionId, type, body) {
|
|
109
|
-
await postActivity(client, sessionId, { type, body });
|
|
123
|
+
async function postBodyActivity(client, sessionId, type, body, options) {
|
|
124
|
+
await postActivity(client, sessionId, { type, body }, options);
|
|
110
125
|
}
|
|
111
|
-
async function postThought(client, sessionId, body) {
|
|
112
|
-
await postBodyActivity(client, sessionId, import_sdk.LinearDocument.AgentActivityType.Thought, body);
|
|
126
|
+
async function postThought(client, sessionId, body, options) {
|
|
127
|
+
await postBodyActivity(client, sessionId, import_sdk.LinearDocument.AgentActivityType.Thought, body, options);
|
|
113
128
|
}
|
|
114
|
-
async function postError(client, sessionId, body) {
|
|
115
|
-
await postBodyActivity(client, sessionId, import_sdk.LinearDocument.AgentActivityType.Error, body);
|
|
129
|
+
async function postError(client, sessionId, body, options) {
|
|
130
|
+
await postBodyActivity(client, sessionId, import_sdk.LinearDocument.AgentActivityType.Error, body, options);
|
|
116
131
|
}
|
|
117
132
|
async function postResponse(client, sessionId, body) {
|
|
118
133
|
await postBodyActivity(client, sessionId, import_sdk.LinearDocument.AgentActivityType.Response, body);
|
|
@@ -120,14 +135,53 @@ async function postResponse(client, sessionId, body) {
|
|
|
120
135
|
async function postElicitation(client, sessionId, body) {
|
|
121
136
|
await postBodyActivity(client, sessionId, import_sdk.LinearDocument.AgentActivityType.Elicitation, body);
|
|
122
137
|
}
|
|
123
|
-
async function
|
|
138
|
+
async function postSelectElicitation(client, sessionId, body, options) {
|
|
139
|
+
await postActivity(client, sessionId, {
|
|
140
|
+
type: import_sdk.LinearDocument.AgentActivityType.Elicitation,
|
|
141
|
+
body,
|
|
142
|
+
signal: import_sdk.LinearDocument.AgentActivitySignal.Select,
|
|
143
|
+
signalMetadata: { options }
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async function postAuthElicitation(client, sessionId, body, url, provider) {
|
|
147
|
+
await postActivity(client, sessionId, {
|
|
148
|
+
type: import_sdk.LinearDocument.AgentActivityType.Elicitation,
|
|
149
|
+
body,
|
|
150
|
+
signal: import_sdk.LinearDocument.AgentActivitySignal.Auth,
|
|
151
|
+
signalMetadata: {
|
|
152
|
+
url,
|
|
153
|
+
...provider ? { provider } : {}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async function postAction(client, sessionId, body, options) {
|
|
124
158
|
await postActivity(client, sessionId, {
|
|
125
159
|
type: import_sdk.LinearDocument.AgentActivityType.Action,
|
|
126
160
|
action: body,
|
|
127
161
|
parameter: ""
|
|
128
|
-
});
|
|
162
|
+
}, options);
|
|
129
163
|
}
|
|
164
|
+
var PLAN_STATUS_MAP = {
|
|
165
|
+
pending: "pending",
|
|
166
|
+
in_progress: "inProgress",
|
|
167
|
+
completed: "completed",
|
|
168
|
+
failed: "canceled"
|
|
169
|
+
};
|
|
130
170
|
async function updatePlan(client, sessionId, steps) {
|
|
171
|
+
if (typeof client.updateAgentSession === "function") {
|
|
172
|
+
const plan = {
|
|
173
|
+
steps: steps.map((step) => ({
|
|
174
|
+
content: step.title,
|
|
175
|
+
status: PLAN_STATUS_MAP[step.status]
|
|
176
|
+
}))
|
|
177
|
+
};
|
|
178
|
+
try {
|
|
179
|
+
await client.updateAgentSession(sessionId, { plan });
|
|
180
|
+
return;
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.warn("updateAgentSession failed, falling back to legacy plan", err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
131
185
|
const planSummary = steps.map((step) => {
|
|
132
186
|
const icon = step.status === "completed" ? "\u2705" : step.status === "in_progress" ? "\u23F3" : step.status === "failed" ? "\u274C" : "\u2B1C";
|
|
133
187
|
return `${icon} ${step.title}`;
|
|
@@ -139,6 +193,141 @@ ${planSummary}`
|
|
|
139
193
|
});
|
|
140
194
|
}
|
|
141
195
|
|
|
196
|
+
// src/integrations/linear/types.ts
|
|
197
|
+
var STALE_SESSION_CHECK_INTERVAL_MS = 20 * 6e4;
|
|
198
|
+
var STALE_SESSION_THRESHOLD_MS = 25 * 6e4;
|
|
199
|
+
|
|
200
|
+
// src/integrations/linear/stale-session-guard.ts
|
|
201
|
+
var DEFAULT_KEEPALIVE_MESSAGE = "Still working \u2014 waiting for specialist output in the collaboration room.";
|
|
202
|
+
var StaleSessionGuard = class {
|
|
203
|
+
store;
|
|
204
|
+
linearClient;
|
|
205
|
+
logger;
|
|
206
|
+
checkIntervalMs;
|
|
207
|
+
staleThresholdMs;
|
|
208
|
+
keepAliveMessage;
|
|
209
|
+
timer = null;
|
|
210
|
+
constructor(options) {
|
|
211
|
+
this.store = options.store;
|
|
212
|
+
this.linearClient = options.linearClient;
|
|
213
|
+
this.logger = options.logger ?? new NoopLogger();
|
|
214
|
+
this.checkIntervalMs = options.checkIntervalMs ?? STALE_SESSION_CHECK_INTERVAL_MS;
|
|
215
|
+
this.staleThresholdMs = options.staleThresholdMs ?? STALE_SESSION_THRESHOLD_MS;
|
|
216
|
+
this.keepAliveMessage = options.keepAliveMessage ?? DEFAULT_KEEPALIVE_MESSAGE;
|
|
217
|
+
}
|
|
218
|
+
/** Start the periodic keepalive check. Safe to call multiple times. */
|
|
219
|
+
start() {
|
|
220
|
+
if (this.timer) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
this.logger.info("stale_session_guard.started", {
|
|
224
|
+
checkIntervalMs: this.checkIntervalMs,
|
|
225
|
+
staleThresholdMs: this.staleThresholdMs
|
|
226
|
+
});
|
|
227
|
+
this.timer = setInterval(() => {
|
|
228
|
+
void this.tick();
|
|
229
|
+
}, this.checkIntervalMs);
|
|
230
|
+
if (typeof this.timer === "object" && "unref" in this.timer) {
|
|
231
|
+
this.timer.unref();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** Stop the periodic keepalive check. */
|
|
235
|
+
stop() {
|
|
236
|
+
if (!this.timer) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
clearInterval(this.timer);
|
|
240
|
+
this.timer = null;
|
|
241
|
+
this.logger.info("stale_session_guard.stopped", {});
|
|
242
|
+
}
|
|
243
|
+
/** Run a single keepalive check. Exposed for testing and manual invocation. */
|
|
244
|
+
async tick() {
|
|
245
|
+
if (!this.store.listActiveSessions) {
|
|
246
|
+
this.logger.warn("stale_session_guard.store_missing_listActiveSessions", {});
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
let sessions;
|
|
250
|
+
try {
|
|
251
|
+
sessions = await this.store.listActiveSessions();
|
|
252
|
+
} catch (error) {
|
|
253
|
+
this.logger.error("stale_session_guard.list_sessions_failed", {
|
|
254
|
+
error: error instanceof Error ? error.message : String(error)
|
|
255
|
+
});
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
let kept = 0;
|
|
260
|
+
for (const session of sessions) {
|
|
261
|
+
if (isSessionStale(session, now, this.staleThresholdMs)) {
|
|
262
|
+
try {
|
|
263
|
+
await postThought(this.linearClient, session.linearSessionId, this.keepAliveMessage);
|
|
264
|
+
const activityTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
265
|
+
await this.store.upsert({
|
|
266
|
+
...session,
|
|
267
|
+
lastLinearActivityAt: activityTimestamp,
|
|
268
|
+
updatedAt: activityTimestamp
|
|
269
|
+
});
|
|
270
|
+
kept += 1;
|
|
271
|
+
this.logger.info("stale_session_guard.keepalive_sent", {
|
|
272
|
+
sessionId: session.linearSessionId,
|
|
273
|
+
lastLinearActivityAt: session.lastLinearActivityAt
|
|
274
|
+
});
|
|
275
|
+
} catch (error) {
|
|
276
|
+
this.logger.warn("stale_session_guard.keepalive_failed", {
|
|
277
|
+
sessionId: session.linearSessionId,
|
|
278
|
+
error: error instanceof Error ? error.message : String(error)
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (kept > 0) {
|
|
284
|
+
this.logger.info("stale_session_guard.tick_complete", {
|
|
285
|
+
totalSessions: sessions.length,
|
|
286
|
+
keepalivesSent: kept
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return kept;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
function isSessionStale(session, nowMs, thresholdMs) {
|
|
293
|
+
const referenceIso = session.lastLinearActivityAt ?? session.updatedAt;
|
|
294
|
+
const referenceMs = new Date(referenceIso).getTime();
|
|
295
|
+
if (Number.isNaN(referenceMs)) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
return nowMs - referenceMs >= thresholdMs;
|
|
299
|
+
}
|
|
300
|
+
async function sendRecoveryActivityIfStale(input) {
|
|
301
|
+
const thresholdMs = input.staleThresholdMs ?? STALE_SESSION_THRESHOLD_MS;
|
|
302
|
+
if (!isSessionStale(input.session, Date.now(), thresholdMs)) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
await postThought(
|
|
307
|
+
input.linearClient,
|
|
308
|
+
input.session.linearSessionId,
|
|
309
|
+
"Resuming session \u2014 reconnecting after extended specialist work."
|
|
310
|
+
);
|
|
311
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
312
|
+
await input.store.upsert({
|
|
313
|
+
...input.session,
|
|
314
|
+
lastLinearActivityAt: now,
|
|
315
|
+
updatedAt: now
|
|
316
|
+
});
|
|
317
|
+
input.logger.info("stale_session_guard.recovery_activity_sent", {
|
|
318
|
+
sessionId: input.session.linearSessionId,
|
|
319
|
+
lastLinearActivityAt: input.session.lastLinearActivityAt
|
|
320
|
+
});
|
|
321
|
+
return true;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
input.logger.warn("stale_session_guard.recovery_activity_failed", {
|
|
324
|
+
sessionId: input.session.linearSessionId,
|
|
325
|
+
error: error instanceof Error ? error.message : String(error)
|
|
326
|
+
});
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
142
331
|
// src/integrations/linear/bridge/message.ts
|
|
143
332
|
function buildBridgeMessage(input) {
|
|
144
333
|
const header = input.action === "created" ? "[Linear]: Agent session created." : "[Linear]: Agent session updated.";
|
|
@@ -158,6 +347,8 @@ function buildBridgeMessage(input) {
|
|
|
158
347
|
const issueStateTypeLine = input.issueStateType ? `issue_state_type: ${input.issueStateType}` : "issue_state_type: none";
|
|
159
348
|
const issueAssigneeLine = input.issueAssigneeName ? `issue_assignee: ${input.issueAssigneeName}` : "issue_assignee: none";
|
|
160
349
|
const issueAssigneeIdLine = input.issueAssigneeId ? `issue_assignee_id: ${input.issueAssigneeId}` : "issue_assignee_id: none";
|
|
350
|
+
const issueDelegateLine = input.issueDelegateName ? `issue_delegate: ${input.issueDelegateName}` : "issue_delegate: none";
|
|
351
|
+
const issueDelegateIdLine = input.issueDelegateId ? `issue_delegate_id: ${input.issueDelegateId}` : "issue_delegate_id: none";
|
|
161
352
|
const sessionStatusLine = input.sessionStatus ? `session_status: ${input.sessionStatus}` : "session_status: none";
|
|
162
353
|
const sessionTypeLine = input.sessionType ? `session_type: ${input.sessionType}` : "session_type: none";
|
|
163
354
|
const sessionCreatedLine = input.sessionCreatedAt ? `session_created_at: ${input.sessionCreatedAt}` : "session_created_at: none";
|
|
@@ -193,6 +384,8 @@ Linear session context:
|
|
|
193
384
|
- ${issueStateTypeLine}
|
|
194
385
|
- ${issueAssigneeLine}
|
|
195
386
|
- ${issueAssigneeIdLine}
|
|
387
|
+
- ${issueDelegateLine}
|
|
388
|
+
- ${issueDelegateIdLine}
|
|
196
389
|
- inferred_session_intent: ${input.sessionIntent}
|
|
197
390
|
- writeback_mode: ${input.writebackMode}
|
|
198
391
|
- ${appUserLine}
|
|
@@ -278,6 +471,11 @@ function extractIssueAssigneeField(issue, field) {
|
|
|
278
471
|
const value = assignee?.[field];
|
|
279
472
|
return typeof value === "string" ? value : null;
|
|
280
473
|
}
|
|
474
|
+
function extractIssueDelegateField(issue, field) {
|
|
475
|
+
const delegate = extractNestedRecord(issue, "delegate");
|
|
476
|
+
const value = delegate?.[field];
|
|
477
|
+
return typeof value === "string" ? value : null;
|
|
478
|
+
}
|
|
281
479
|
function firstNonEmpty(...values) {
|
|
282
480
|
for (const value of values) {
|
|
283
481
|
if (!value) {
|
|
@@ -300,11 +498,13 @@ function extractRecord(value) {
|
|
|
300
498
|
}
|
|
301
499
|
|
|
302
500
|
// src/integrations/linear/bridge/handler.ts
|
|
501
|
+
var DEFAULT_THENVOI_APP_BASE_URL = "https://app.thenvoi.com";
|
|
303
502
|
var SUPPORTED_ACTIONS = /* @__PURE__ */ new Set(["created", "updated", "canceled", "prompted"]);
|
|
304
503
|
var MAX_PEER_LOOKUP_PAGES = 25;
|
|
305
504
|
var PEER_PAGE_SIZE = 100;
|
|
306
505
|
var RECOVERED_ROOM_EVENT_RETRY_LIMIT = 2;
|
|
307
506
|
var RECOVERED_ROOM_EVENT_RETRY_BASE_DELAY_MS = 1e3;
|
|
507
|
+
var AUTO_START_ELIGIBLE_TYPES = /* @__PURE__ */ new Set(["backlog", "unstarted", "triage"]);
|
|
308
508
|
function createLinearBridgeRuntime() {
|
|
309
509
|
return {
|
|
310
510
|
roomResolutionLocks: /* @__PURE__ */ new Map(),
|
|
@@ -369,9 +569,19 @@ async function handleAgentSessionEvent(input, options) {
|
|
|
369
569
|
});
|
|
370
570
|
return;
|
|
371
571
|
}
|
|
572
|
+
if (action === "updated" && existingBySession) {
|
|
573
|
+
await sendRecoveryActivityIfStale({
|
|
574
|
+
session: existingBySession,
|
|
575
|
+
linearClient: input.deps.linearClient,
|
|
576
|
+
store: input.deps.store,
|
|
577
|
+
logger
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
let lastLinearActivityAt = null;
|
|
372
581
|
if (action === "created" && !options?.skipAcknowledgment) {
|
|
373
582
|
try {
|
|
374
583
|
await postThought(input.deps.linearClient, sessionId, "Received session. Setting up workspace...");
|
|
584
|
+
lastLinearActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
375
585
|
} catch (ackError) {
|
|
376
586
|
logger.warn("linear_thenvoi_bridge.acknowledgment_failed", {
|
|
377
587
|
sessionId,
|
|
@@ -379,7 +589,49 @@ async function handleAgentSessionEvent(input, options) {
|
|
|
379
589
|
});
|
|
380
590
|
}
|
|
381
591
|
}
|
|
592
|
+
let delegatePromise;
|
|
593
|
+
if (action === "created" && issueId) {
|
|
594
|
+
const appUserId = input.payload.appUserId;
|
|
595
|
+
if (appUserId) {
|
|
596
|
+
delegatePromise = trySetAgentAsDelegate({
|
|
597
|
+
linearClient: input.deps.linearClient,
|
|
598
|
+
issueId,
|
|
599
|
+
appUserId,
|
|
600
|
+
logger
|
|
601
|
+
}).catch((delegateError) => {
|
|
602
|
+
logger.warn("linear_thenvoi_bridge.auto_delegate_failed", {
|
|
603
|
+
sessionId,
|
|
604
|
+
issueId,
|
|
605
|
+
appUserId,
|
|
606
|
+
error: delegateError instanceof Error ? delegateError.message : String(delegateError)
|
|
607
|
+
});
|
|
608
|
+
return { set: false, delegateName: null };
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
let autoStartPromise;
|
|
613
|
+
if (action === "created" && issueId) {
|
|
614
|
+
const originalStateType = extractIssueStateField(input.payload.agentSession.issue, "type");
|
|
615
|
+
const teamId = extractIssueTeamId(input.payload.agentSession.issue);
|
|
616
|
+
if (teamId && originalStateType && AUTO_START_ELIGIBLE_TYPES.has(originalStateType)) {
|
|
617
|
+
autoStartPromise = tryMoveIssueToStarted({
|
|
618
|
+
linearClient: input.deps.linearClient,
|
|
619
|
+
issueId,
|
|
620
|
+
teamId,
|
|
621
|
+
logger
|
|
622
|
+
}).catch((autoStartError) => {
|
|
623
|
+
logger.warn("linear_thenvoi_bridge.auto_start_failed", {
|
|
624
|
+
sessionId,
|
|
625
|
+
issueId,
|
|
626
|
+
teamId,
|
|
627
|
+
error: autoStartError instanceof Error ? autoStartError.message : String(autoStartError)
|
|
628
|
+
});
|
|
629
|
+
return { moved: false, stateId: null, stateName: null };
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
382
633
|
let roomRecord = null;
|
|
634
|
+
let externalUrlPromise;
|
|
383
635
|
try {
|
|
384
636
|
const hostAgentHandle = await resolveHostAgentHandle({
|
|
385
637
|
thenvoiRest: input.deps.thenvoiRest,
|
|
@@ -397,6 +649,22 @@ async function handleAgentSessionEvent(input, options) {
|
|
|
397
649
|
logger,
|
|
398
650
|
runtime
|
|
399
651
|
});
|
|
652
|
+
if (action === "created") {
|
|
653
|
+
const roomId = roomRecord.thenvoiRoomId;
|
|
654
|
+
externalUrlPromise = trySetSessionExternalUrl({
|
|
655
|
+
linearClient: input.deps.linearClient,
|
|
656
|
+
sessionId,
|
|
657
|
+
roomId,
|
|
658
|
+
appBaseUrl: config.thenvoiAppBaseUrl,
|
|
659
|
+
logger
|
|
660
|
+
}).catch((urlError) => {
|
|
661
|
+
logger.warn("linear_thenvoi_bridge.set_external_url_failed", {
|
|
662
|
+
sessionId,
|
|
663
|
+
roomId,
|
|
664
|
+
error: urlError instanceof Error ? urlError.message : String(urlError)
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
}
|
|
400
668
|
const sessionIntent = detectSessionIntent({
|
|
401
669
|
issueStateType: extractIssueStateField(input.payload.agentSession.issue, "type"),
|
|
402
670
|
promptContext: input.payload.promptContext,
|
|
@@ -419,6 +687,22 @@ async function handleAgentSessionEvent(input, options) {
|
|
|
419
687
|
handles: [hostAgentHandle],
|
|
420
688
|
logger
|
|
421
689
|
});
|
|
690
|
+
const delegateResult = await delegatePromise;
|
|
691
|
+
let issueDelegateId = extractIssueDelegateField(input.payload.agentSession.issue, "id");
|
|
692
|
+
let issueDelegateName = extractIssueDelegateField(input.payload.agentSession.issue, "displayName") ?? extractIssueDelegateField(input.payload.agentSession.issue, "name");
|
|
693
|
+
if (delegateResult?.set && input.payload.appUserId) {
|
|
694
|
+
issueDelegateId = input.payload.appUserId;
|
|
695
|
+
issueDelegateName = delegateResult.delegateName ?? input.payload.appUserId;
|
|
696
|
+
}
|
|
697
|
+
const autoStartResult = await autoStartPromise;
|
|
698
|
+
let issueStateId = extractIssueStateField(input.payload.agentSession.issue, "id");
|
|
699
|
+
let issueStateName = extractIssueStateField(input.payload.agentSession.issue, "name");
|
|
700
|
+
let resolvedStateType = extractIssueStateField(input.payload.agentSession.issue, "type");
|
|
701
|
+
if (autoStartResult?.moved) {
|
|
702
|
+
if (autoStartResult.stateId) issueStateId = autoStartResult.stateId;
|
|
703
|
+
if (autoStartResult.stateName) issueStateName = autoStartResult.stateName;
|
|
704
|
+
resolvedStateType = "started";
|
|
705
|
+
}
|
|
422
706
|
const message = buildBridgeMessage({
|
|
423
707
|
sessionId,
|
|
424
708
|
issueId,
|
|
@@ -436,11 +720,13 @@ async function handleAgentSessionEvent(input, options) {
|
|
|
436
720
|
issueTeamKey: extractIssueTeamKey(input.payload.agentSession.issue),
|
|
437
721
|
issueTeamName: extractIssueTeamName(input.payload.agentSession.issue),
|
|
438
722
|
issueTeamId: extractIssueTeamId(input.payload.agentSession.issue),
|
|
439
|
-
issueStateId
|
|
440
|
-
issueStateName
|
|
441
|
-
issueStateType:
|
|
723
|
+
issueStateId,
|
|
724
|
+
issueStateName,
|
|
725
|
+
issueStateType: resolvedStateType,
|
|
442
726
|
issueAssigneeId: extractIssueAssigneeField(input.payload.agentSession.issue, "id"),
|
|
443
727
|
issueAssigneeName: extractIssueAssigneeField(input.payload.agentSession.issue, "displayName") ?? extractIssueAssigneeField(input.payload.agentSession.issue, "name"),
|
|
728
|
+
issueDelegateId,
|
|
729
|
+
issueDelegateName,
|
|
444
730
|
commentBody: input.payload.agentSession.comment?.body,
|
|
445
731
|
commentId: input.payload.agentSession.comment?.id,
|
|
446
732
|
sessionIntent,
|
|
@@ -497,12 +783,15 @@ async function handleAgentSessionEvent(input, options) {
|
|
|
497
783
|
} : messageMetadata
|
|
498
784
|
});
|
|
499
785
|
}
|
|
786
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
500
787
|
await saveSessionRecord(input.deps.store, {
|
|
501
788
|
...roomRecord,
|
|
502
789
|
status: "active",
|
|
503
790
|
lastEventKey: eventKey,
|
|
504
|
-
|
|
791
|
+
lastLinearActivityAt: lastLinearActivityAt ?? roomRecord.lastLinearActivityAt ?? now,
|
|
792
|
+
updatedAt: now
|
|
505
793
|
});
|
|
794
|
+
await externalUrlPromise;
|
|
506
795
|
logger.info("linear_thenvoi_bridge.message_forwarded", {
|
|
507
796
|
sessionId,
|
|
508
797
|
issueId,
|
|
@@ -510,6 +799,9 @@ async function handleAgentSessionEvent(input, options) {
|
|
|
510
799
|
action
|
|
511
800
|
});
|
|
512
801
|
} catch (error) {
|
|
802
|
+
await externalUrlPromise;
|
|
803
|
+
await delegatePromise;
|
|
804
|
+
await autoStartPromise;
|
|
513
805
|
try {
|
|
514
806
|
await postError(
|
|
515
807
|
input.deps.linearClient,
|
|
@@ -555,18 +847,39 @@ async function completeLinearSession(input) {
|
|
|
555
847
|
if (!existing) {
|
|
556
848
|
return;
|
|
557
849
|
}
|
|
850
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
558
851
|
await saveSessionRecord(input.store, {
|
|
559
852
|
...existing,
|
|
560
853
|
status: "completed",
|
|
561
854
|
lastEventKey: input.lastEventKey ?? existing.lastEventKey ?? null,
|
|
562
|
-
|
|
855
|
+
lastLinearActivityAt: now,
|
|
856
|
+
updatedAt: now
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
async function trySetSessionExternalUrl(input) {
|
|
860
|
+
if (typeof input.linearClient.agentSessionUpdateExternalUrl !== "function") {
|
|
861
|
+
input.logger.info("linear_thenvoi_bridge.set_external_url_skipped_no_api", {
|
|
862
|
+
sessionId: input.sessionId
|
|
863
|
+
});
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const base = input.appBaseUrl.replace(/\/+$/, "");
|
|
867
|
+
const roomUrl = `${base}/rooms/${input.roomId}`;
|
|
868
|
+
await input.linearClient.agentSessionUpdateExternalUrl(input.sessionId, {
|
|
869
|
+
externalUrls: [{ label: "View in Thenvoi", url: roomUrl }]
|
|
870
|
+
});
|
|
871
|
+
input.logger.info("linear_thenvoi_bridge.external_url_set", {
|
|
872
|
+
sessionId: input.sessionId,
|
|
873
|
+
roomId: input.roomId,
|
|
874
|
+
url: roomUrl
|
|
563
875
|
});
|
|
564
876
|
}
|
|
565
877
|
function normalizeConfig(config) {
|
|
566
878
|
return {
|
|
567
879
|
roomStrategy: config.roomStrategy ?? "issue",
|
|
568
880
|
writebackMode: config.writebackMode ?? "final_only",
|
|
569
|
-
hostAgentHandle: normalizeOptionalHandle(config.hostAgentHandle)
|
|
881
|
+
hostAgentHandle: normalizeOptionalHandle(config.hostAgentHandle),
|
|
882
|
+
thenvoiAppBaseUrl: config.thenvoiAppBaseUrl ?? DEFAULT_THENVOI_APP_BASE_URL
|
|
570
883
|
};
|
|
571
884
|
}
|
|
572
885
|
function normalizeOptionalHandle(handle) {
|
|
@@ -576,6 +889,63 @@ function normalizeOptionalHandle(handle) {
|
|
|
576
889
|
const normalized = stripHandlePrefix(handle);
|
|
577
890
|
return normalized.length > 0 ? normalized : null;
|
|
578
891
|
}
|
|
892
|
+
async function trySetAgentAsDelegate(input) {
|
|
893
|
+
const issue = await input.linearClient.issue(input.issueId);
|
|
894
|
+
const existingDelegateId = issue.delegateId;
|
|
895
|
+
if (existingDelegateId) {
|
|
896
|
+
input.logger.info("linear_thenvoi_bridge.delegate_already_set", {
|
|
897
|
+
issueId: input.issueId,
|
|
898
|
+
existingDelegateId
|
|
899
|
+
});
|
|
900
|
+
return { set: false, delegateName: null };
|
|
901
|
+
}
|
|
902
|
+
await input.linearClient.updateIssue(input.issueId, {
|
|
903
|
+
delegateId: input.appUserId
|
|
904
|
+
});
|
|
905
|
+
let delegateName = null;
|
|
906
|
+
try {
|
|
907
|
+
const updated = await input.linearClient.issue(input.issueId);
|
|
908
|
+
const delegate = await updated.delegate;
|
|
909
|
+
delegateName = delegate?.displayName ?? delegate?.name ?? null;
|
|
910
|
+
} catch {
|
|
911
|
+
}
|
|
912
|
+
input.logger.info("linear_thenvoi_bridge.delegate_set", {
|
|
913
|
+
issueId: input.issueId,
|
|
914
|
+
delegateId: input.appUserId
|
|
915
|
+
});
|
|
916
|
+
return { set: true, delegateName };
|
|
917
|
+
}
|
|
918
|
+
async function tryMoveIssueToStarted(input) {
|
|
919
|
+
const response = await input.linearClient.workflowStates({
|
|
920
|
+
filter: {
|
|
921
|
+
team: { id: { eq: input.teamId } },
|
|
922
|
+
type: { eq: "started" }
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
const nodes = Array.isArray(response.nodes) ? response.nodes : [];
|
|
926
|
+
const startedStates = [...nodes].sort((a, b) => a.position - b.position);
|
|
927
|
+
const targetState = startedStates[0];
|
|
928
|
+
if (!targetState?.id) {
|
|
929
|
+
input.logger.info("linear_thenvoi_bridge.auto_start_no_started_state", {
|
|
930
|
+
issueId: input.issueId,
|
|
931
|
+
teamId: input.teamId
|
|
932
|
+
});
|
|
933
|
+
return { moved: false, stateId: null, stateName: null };
|
|
934
|
+
}
|
|
935
|
+
await input.linearClient.updateIssue(input.issueId, {
|
|
936
|
+
stateId: targetState.id
|
|
937
|
+
});
|
|
938
|
+
input.logger.info("linear_thenvoi_bridge.auto_start_moved", {
|
|
939
|
+
issueId: input.issueId,
|
|
940
|
+
stateId: targetState.id,
|
|
941
|
+
stateName: targetState.name ?? null
|
|
942
|
+
});
|
|
943
|
+
return {
|
|
944
|
+
moved: true,
|
|
945
|
+
stateId: targetState.id,
|
|
946
|
+
stateName: targetState.name ?? null
|
|
947
|
+
};
|
|
948
|
+
}
|
|
579
949
|
async function resolveHostAgentHandle(input) {
|
|
580
950
|
if (input.configuredHostHandle) {
|
|
581
951
|
input.runtime.resolvedHostHandleCache.set(input.thenvoiRest, input.configuredHostHandle);
|
|
@@ -661,6 +1031,12 @@ async function handlePromptedAction(input) {
|
|
|
661
1031
|
});
|
|
662
1032
|
return;
|
|
663
1033
|
}
|
|
1034
|
+
await sendRecoveryActivityIfStale({
|
|
1035
|
+
session: existing,
|
|
1036
|
+
linearClient: input.deps.linearClient,
|
|
1037
|
+
store: input.deps.store,
|
|
1038
|
+
logger: input.logger
|
|
1039
|
+
});
|
|
664
1040
|
const message = `[Linear User Response]: ${userResponse}`;
|
|
665
1041
|
const metadata = {
|
|
666
1042
|
linear_event_action: "prompted",
|
|
@@ -1127,6 +1503,7 @@ var SqliteSessionRoomStore = class {
|
|
|
1127
1503
|
thenvoi_room_id,
|
|
1128
1504
|
status,
|
|
1129
1505
|
last_event_key,
|
|
1506
|
+
last_linear_activity_at,
|
|
1130
1507
|
created_at,
|
|
1131
1508
|
updated_at
|
|
1132
1509
|
FROM linear_thenvoi_session_rooms
|
|
@@ -1147,6 +1524,7 @@ var SqliteSessionRoomStore = class {
|
|
|
1147
1524
|
thenvoi_room_id,
|
|
1148
1525
|
status,
|
|
1149
1526
|
last_event_key,
|
|
1527
|
+
last_linear_activity_at,
|
|
1150
1528
|
created_at,
|
|
1151
1529
|
updated_at
|
|
1152
1530
|
FROM linear_thenvoi_session_rooms
|
|
@@ -1168,15 +1546,17 @@ var SqliteSessionRoomStore = class {
|
|
|
1168
1546
|
thenvoi_room_id,
|
|
1169
1547
|
status,
|
|
1170
1548
|
last_event_key,
|
|
1549
|
+
last_linear_activity_at,
|
|
1171
1550
|
created_at,
|
|
1172
1551
|
updated_at
|
|
1173
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1552
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1174
1553
|
ON CONFLICT(linear_session_id)
|
|
1175
1554
|
DO UPDATE SET
|
|
1176
1555
|
linear_issue_id = excluded.linear_issue_id,
|
|
1177
1556
|
thenvoi_room_id = excluded.thenvoi_room_id,
|
|
1178
1557
|
status = excluded.status,
|
|
1179
1558
|
last_event_key = excluded.last_event_key,
|
|
1559
|
+
last_linear_activity_at = excluded.last_linear_activity_at,
|
|
1180
1560
|
updated_at = excluded.updated_at
|
|
1181
1561
|
`
|
|
1182
1562
|
).run(
|
|
@@ -1185,6 +1565,7 @@ var SqliteSessionRoomStore = class {
|
|
|
1185
1565
|
record.thenvoiRoomId,
|
|
1186
1566
|
record.status,
|
|
1187
1567
|
record.lastEventKey ?? null,
|
|
1568
|
+
record.lastLinearActivityAt ?? null,
|
|
1188
1569
|
record.createdAt,
|
|
1189
1570
|
record.updatedAt
|
|
1190
1571
|
);
|
|
@@ -1278,6 +1659,29 @@ var SqliteSessionRoomStore = class {
|
|
|
1278
1659
|
`
|
|
1279
1660
|
).run((/* @__PURE__ */ new Date()).toISOString(), eventKey);
|
|
1280
1661
|
}
|
|
1662
|
+
async listActiveSessions() {
|
|
1663
|
+
const db = await this.getDb();
|
|
1664
|
+
const rawRows = db.prepare(
|
|
1665
|
+
`
|
|
1666
|
+
SELECT
|
|
1667
|
+
linear_session_id,
|
|
1668
|
+
linear_issue_id,
|
|
1669
|
+
thenvoi_room_id,
|
|
1670
|
+
status,
|
|
1671
|
+
last_event_key,
|
|
1672
|
+
last_linear_activity_at,
|
|
1673
|
+
created_at,
|
|
1674
|
+
updated_at
|
|
1675
|
+
FROM linear_thenvoi_session_rooms
|
|
1676
|
+
WHERE status IN ('active', 'waiting')
|
|
1677
|
+
ORDER BY updated_at DESC
|
|
1678
|
+
`
|
|
1679
|
+
).all();
|
|
1680
|
+
if (!Array.isArray(rawRows)) {
|
|
1681
|
+
return [];
|
|
1682
|
+
}
|
|
1683
|
+
return rawRows.map((raw) => parseSessionRoomRow(raw)).filter((row) => row !== null).map((row) => this.toRecord(row));
|
|
1684
|
+
}
|
|
1281
1685
|
async close() {
|
|
1282
1686
|
if (!this.dbPromise) {
|
|
1283
1687
|
return;
|
|
@@ -1359,6 +1763,16 @@ var SqliteSessionRoomStore = class {
|
|
|
1359
1763
|
throw error;
|
|
1360
1764
|
}
|
|
1361
1765
|
}
|
|
1766
|
+
try {
|
|
1767
|
+
db.exec(`
|
|
1768
|
+
ALTER TABLE linear_thenvoi_session_rooms
|
|
1769
|
+
ADD COLUMN last_linear_activity_at TEXT
|
|
1770
|
+
`);
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
if (!isDuplicateColumnError(error)) {
|
|
1773
|
+
throw error;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1362
1776
|
return db;
|
|
1363
1777
|
}
|
|
1364
1778
|
toRecord(row) {
|
|
@@ -1368,6 +1782,7 @@ var SqliteSessionRoomStore = class {
|
|
|
1368
1782
|
thenvoiRoomId: row.thenvoi_room_id,
|
|
1369
1783
|
status: row.status,
|
|
1370
1784
|
lastEventKey: row.last_event_key,
|
|
1785
|
+
lastLinearActivityAt: row.last_linear_activity_at,
|
|
1371
1786
|
createdAt: row.created_at,
|
|
1372
1787
|
updatedAt: row.updated_at
|
|
1373
1788
|
};
|
|
@@ -1423,9 +1838,10 @@ function parseSessionRoomRow(value) {
|
|
|
1423
1838
|
const thenvoiRoomId = asString(row.thenvoi_room_id);
|
|
1424
1839
|
const status = asString(row.status);
|
|
1425
1840
|
const lastEventKey = asNullableString(row.last_event_key);
|
|
1841
|
+
const lastLinearActivityAt = asNullableString(row.last_linear_activity_at);
|
|
1426
1842
|
const createdAt = asString(row.created_at);
|
|
1427
1843
|
const updatedAt = asString(row.updated_at);
|
|
1428
|
-
if (!linearSessionId || linearIssueId === void 0 || !thenvoiRoomId || !status || !isSessionStatus(status) || lastEventKey === void 0 || !createdAt || !updatedAt) {
|
|
1844
|
+
if (!linearSessionId || linearIssueId === void 0 || !thenvoiRoomId || !status || !isSessionStatus(status) || lastEventKey === void 0 || lastLinearActivityAt === void 0 || !createdAt || !updatedAt) {
|
|
1429
1845
|
return null;
|
|
1430
1846
|
}
|
|
1431
1847
|
return {
|
|
@@ -1434,6 +1850,7 @@ function parseSessionRoomRow(value) {
|
|
|
1434
1850
|
thenvoi_room_id: thenvoiRoomId,
|
|
1435
1851
|
status,
|
|
1436
1852
|
last_event_key: lastEventKey,
|
|
1853
|
+
last_linear_activity_at: lastLinearActivityAt,
|
|
1437
1854
|
created_at: createdAt,
|
|
1438
1855
|
updated_at: updatedAt
|
|
1439
1856
|
};
|
|
@@ -1479,8 +1896,18 @@ function createSqliteSessionRoomStore(dbPath) {
|
|
|
1479
1896
|
|
|
1480
1897
|
// src/integrations/linear/tools.ts
|
|
1481
1898
|
var import_zod = require("zod");
|
|
1899
|
+
var LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
1900
|
+
function isAllowedAuthUrl(u) {
|
|
1901
|
+
try {
|
|
1902
|
+
const parsed = new URL(u);
|
|
1903
|
+
if (parsed.protocol === "https:") return true;
|
|
1904
|
+
return LOCALHOST_HOSTNAMES.has(parsed.hostname);
|
|
1905
|
+
} catch {
|
|
1906
|
+
return false;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1482
1909
|
function createLinearTools(options) {
|
|
1483
|
-
const { client, store, enableElicitation = true } = options;
|
|
1910
|
+
const { client, store, enableElicitation = true, logger } = options;
|
|
1484
1911
|
const sessionBodySchema = import_zod.z.object({
|
|
1485
1912
|
session_id: import_zod.z.string().describe("The Linear agent session ID"),
|
|
1486
1913
|
body: import_zod.z.string().describe("The message body in Markdown format")
|
|
@@ -1492,52 +1919,121 @@ function createLinearTools(options) {
|
|
|
1492
1919
|
const requiredIssueIdSchema = issueIdInputSchema;
|
|
1493
1920
|
const optionalIssueIdSchema = issueIdInputSchema;
|
|
1494
1921
|
const tools = [];
|
|
1495
|
-
const
|
|
1922
|
+
const ephemeralSessionBodySchema = sessionBodySchema.extend({
|
|
1923
|
+
ephemeral: import_zod.z.boolean().optional().describe(
|
|
1924
|
+
'If true, this activity is displayed temporarily and replaced when the next activity arrives. Use for transient status indicators like "Thinking...", "Searching...", or "Waiting for response...".'
|
|
1925
|
+
)
|
|
1926
|
+
});
|
|
1927
|
+
const addSessionBodyTool = (name, description, activityFn, options2) => {
|
|
1928
|
+
const supportsEphemeral = options2?.supportsEphemeral ?? false;
|
|
1496
1929
|
tools.push({
|
|
1497
1930
|
name,
|
|
1498
|
-
description,
|
|
1499
|
-
schema: sessionBodySchema,
|
|
1500
|
-
handler
|
|
1931
|
+
description: supportsEphemeral ? description + " Set ephemeral: true for transient status updates that should disappear when the next activity arrives." : description,
|
|
1932
|
+
schema: supportsEphemeral ? ephemeralSessionBodySchema : sessionBodySchema,
|
|
1933
|
+
handler: async (args) => {
|
|
1934
|
+
await activityFn(
|
|
1935
|
+
client,
|
|
1936
|
+
args.session_id,
|
|
1937
|
+
args.body,
|
|
1938
|
+
supportsEphemeral && args.ephemeral === true ? { ephemeral: true } : void 0
|
|
1939
|
+
);
|
|
1940
|
+
return { ok: true };
|
|
1941
|
+
}
|
|
1501
1942
|
});
|
|
1502
1943
|
};
|
|
1503
1944
|
addSessionBodyTool(
|
|
1504
1945
|
"linear_post_thought",
|
|
1505
1946
|
"Post a thought to the Linear agent session, visible to the user as internal reasoning.",
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
return { ok: true };
|
|
1509
|
-
}
|
|
1947
|
+
postThought,
|
|
1948
|
+
{ supportsEphemeral: true }
|
|
1510
1949
|
);
|
|
1511
1950
|
addSessionBodyTool(
|
|
1512
1951
|
"linear_post_action",
|
|
1513
1952
|
"Post an action to the Linear agent session, showing the user what step is being taken.",
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
return { ok: true };
|
|
1517
|
-
}
|
|
1953
|
+
postAction,
|
|
1954
|
+
{ supportsEphemeral: true }
|
|
1518
1955
|
);
|
|
1519
1956
|
addSessionBodyTool(
|
|
1520
1957
|
"linear_post_error",
|
|
1521
1958
|
"Post an error to the Linear agent session to notify the user of a failure.",
|
|
1522
|
-
|
|
1523
|
-
await postError(client, args.session_id, args.body);
|
|
1524
|
-
return { ok: true };
|
|
1525
|
-
}
|
|
1959
|
+
postError
|
|
1526
1960
|
);
|
|
1527
1961
|
if (enableElicitation) {
|
|
1528
|
-
|
|
1529
|
-
"linear_ask_user",
|
|
1530
|
-
"Ask the Linear user a question
|
|
1531
|
-
|
|
1532
|
-
|
|
1962
|
+
tools.push({
|
|
1963
|
+
name: "linear_ask_user",
|
|
1964
|
+
description: "Ask the Linear user a question. When options are provided, Linear renders them as a clickable picker (select signal); otherwise the user sees a free-text prompt.",
|
|
1965
|
+
schema: import_zod.z.object({
|
|
1966
|
+
session_id: import_zod.z.string().describe("The Linear agent session ID"),
|
|
1967
|
+
body: import_zod.z.string().max(ELICITATION_BODY_MAX_LENGTH).describe("The question to ask, in Markdown format"),
|
|
1968
|
+
options: import_zod.z.array(import_zod.z.object({
|
|
1969
|
+
label: import_zod.z.string().max(SELECT_OPTION_MAX_LENGTH).describe("Display text for the option"),
|
|
1970
|
+
value: import_zod.z.string().max(SELECT_OPTION_MAX_LENGTH).describe("Value returned when the option is selected")
|
|
1971
|
+
})).min(2).max(20).optional().describe("Clickable options for a select picker (2\u201320 items). Omit for free-text input.")
|
|
1972
|
+
}),
|
|
1973
|
+
handler: async (args) => {
|
|
1974
|
+
const sessionId = args.session_id;
|
|
1975
|
+
const body = args.body;
|
|
1976
|
+
const options2 = args.options;
|
|
1977
|
+
if (options2) {
|
|
1978
|
+
await postSelectElicitation(client, sessionId, body, options2);
|
|
1979
|
+
} else {
|
|
1980
|
+
await postElicitation(client, sessionId, body);
|
|
1981
|
+
}
|
|
1533
1982
|
return { ok: true };
|
|
1534
1983
|
}
|
|
1535
|
-
);
|
|
1984
|
+
});
|
|
1985
|
+
tools.push({
|
|
1986
|
+
name: "linear_select",
|
|
1987
|
+
description: "Present the Linear user with a set of clickable options via a select elicitation. Use this instead of linear_ask_user when the user should pick from a known list of choices.",
|
|
1988
|
+
schema: import_zod.z.object({
|
|
1989
|
+
session_id: import_zod.z.string().describe("The Linear agent session ID"),
|
|
1990
|
+
body: import_zod.z.string().describe("The question or prompt shown above the options (Markdown)"),
|
|
1991
|
+
options: import_zod.z.array(
|
|
1992
|
+
import_zod.z.object({
|
|
1993
|
+
label: import_zod.z.string().describe("Display label for this option"),
|
|
1994
|
+
value: import_zod.z.string().describe("Value returned when this option is selected")
|
|
1995
|
+
})
|
|
1996
|
+
).min(1).max(25).describe("The selectable options")
|
|
1997
|
+
}),
|
|
1998
|
+
handler: async (args) => {
|
|
1999
|
+
await postSelectElicitation(
|
|
2000
|
+
client,
|
|
2001
|
+
args.session_id,
|
|
2002
|
+
args.body,
|
|
2003
|
+
args.options
|
|
2004
|
+
);
|
|
2005
|
+
return { ok: true };
|
|
2006
|
+
}
|
|
2007
|
+
});
|
|
2008
|
+
tools.push({
|
|
2009
|
+
name: "linear_request_auth",
|
|
2010
|
+
description: "Ask the Linear user to link an external account by presenting an authentication button. The user sees a 'Link account' UI that opens the provided URL.",
|
|
2011
|
+
schema: import_zod.z.object({
|
|
2012
|
+
session_id: import_zod.z.string().describe("The Linear agent session ID"),
|
|
2013
|
+
body: import_zod.z.string().max(ELICITATION_BODY_MAX_LENGTH).describe("Explanation of why authentication is needed, in Markdown format"),
|
|
2014
|
+
url: import_zod.z.string().url().refine(
|
|
2015
|
+
isAllowedAuthUrl,
|
|
2016
|
+
{ message: "URL must use https (http allowed only for localhost)" }
|
|
2017
|
+
).describe("The authentication URL to open when the user clicks the link button"),
|
|
2018
|
+
provider: import_zod.z.string().min(1).max(PROVIDER_MAX_LENGTH).optional().describe("Name of the external service (e.g. 'GitHub', 'Slack')")
|
|
2019
|
+
}),
|
|
2020
|
+
handler: async (args) => {
|
|
2021
|
+
await postAuthElicitation(
|
|
2022
|
+
client,
|
|
2023
|
+
args.session_id,
|
|
2024
|
+
args.body,
|
|
2025
|
+
args.url,
|
|
2026
|
+
args.provider
|
|
2027
|
+
);
|
|
2028
|
+
return { ok: true };
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
1536
2031
|
}
|
|
1537
|
-
|
|
1538
|
-
"linear_post_response",
|
|
1539
|
-
"Post the final response to the Linear agent session and mark the session completed when a store is available.",
|
|
1540
|
-
|
|
2032
|
+
tools.push({
|
|
2033
|
+
name: "linear_post_response",
|
|
2034
|
+
description: "Post the final response to the Linear agent session and mark the session completed when a store is available.",
|
|
2035
|
+
schema: sessionBodySchema,
|
|
2036
|
+
handler: async (args) => {
|
|
1541
2037
|
await completeLinearSession({
|
|
1542
2038
|
linearClient: client,
|
|
1543
2039
|
agentSessionId: args.session_id,
|
|
@@ -1546,11 +2042,11 @@ function createLinearTools(options) {
|
|
|
1546
2042
|
});
|
|
1547
2043
|
return { ok: true };
|
|
1548
2044
|
}
|
|
1549
|
-
);
|
|
2045
|
+
});
|
|
1550
2046
|
tools.push(
|
|
1551
2047
|
{
|
|
1552
2048
|
name: "linear_update_plan",
|
|
1553
|
-
description: "Update the plan for the Linear agent session
|
|
2049
|
+
description: "Update the structured plan for the Linear agent session. Renders as a native checklist in the Linear Agent Session UI with live status indicators.",
|
|
1554
2050
|
schema: import_zod.z.object({
|
|
1555
2051
|
session_id: import_zod.z.string().describe("The Linear agent session ID"),
|
|
1556
2052
|
steps: import_zod.z.array(import_zod.z.object({
|
|
@@ -1575,6 +2071,35 @@ function createLinearTools(options) {
|
|
|
1575
2071
|
optionalIssueIdSchema,
|
|
1576
2072
|
issueCommentLimitSchema
|
|
1577
2073
|
});
|
|
2074
|
+
if (typeof client.issueRepositorySuggestions === "function") {
|
|
2075
|
+
const suggestRepositories = client.issueRepositorySuggestions.bind(client);
|
|
2076
|
+
tools.push({
|
|
2077
|
+
name: "linear_suggest_repositories",
|
|
2078
|
+
description: "Ask Linear to rank candidate repositories by relevance for the current issue. Returns ranked suggestions with confidence scores. Use this before asking the user which repository to work in \u2014 if a suggestion has high confidence, auto-select it; otherwise present the top options via the select elicitation signal.",
|
|
2079
|
+
schema: requiredIssueIdSchema.extend({
|
|
2080
|
+
session_id: import_zod.z.string().describe("The Linear agent session ID"),
|
|
2081
|
+
repositories: import_zod.z.array(
|
|
2082
|
+
import_zod.z.object({
|
|
2083
|
+
hostname: import_zod.z.string().describe("Hostname of the Git service (e.g. 'github.com')"),
|
|
2084
|
+
repositoryFullName: import_zod.z.string().describe("Full name in owner/name format (e.g. 'acme/backend')")
|
|
2085
|
+
})
|
|
2086
|
+
).min(1).max(50).describe("Candidate repositories the agent has access to")
|
|
2087
|
+
}),
|
|
2088
|
+
handler: async (args) => {
|
|
2089
|
+
const issueId = resolveIssueId("linear_suggest_repositories", args);
|
|
2090
|
+
const sessionId = args.session_id;
|
|
2091
|
+
const candidates = args.repositories;
|
|
2092
|
+
const response = await suggestRepositories(
|
|
2093
|
+
candidates,
|
|
2094
|
+
issueId,
|
|
2095
|
+
{ agentSessionId: sessionId }
|
|
2096
|
+
);
|
|
2097
|
+
const suggestions = extractRepositorySuggestions(response);
|
|
2098
|
+
return { suggestions };
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
addSessionCreationTools({ tools, client, store, logger });
|
|
1578
2103
|
return tools;
|
|
1579
2104
|
}
|
|
1580
2105
|
function addIssueTools(input) {
|
|
@@ -1628,6 +2153,7 @@ function addIssueTools(input) {
|
|
|
1628
2153
|
priority: import_zod.z.number().int().min(0).max(4).optional().describe("Priority 0-4"),
|
|
1629
2154
|
state_id: import_zod.z.string().optional().describe("Workflow state ID"),
|
|
1630
2155
|
assignee_id: import_zod.z.string().nullable().optional().describe("Assignee user ID, or null to unassign"),
|
|
2156
|
+
delegate_id: import_zod.z.string().nullable().optional().describe("Agent user ID to delegate the issue to, or null to clear delegate"),
|
|
1631
2157
|
estimate: import_zod.z.number().int().optional().describe("Estimate points"),
|
|
1632
2158
|
due_date: import_zod.z.string().optional().describe("Due date ISO string")
|
|
1633
2159
|
}),
|
|
@@ -1636,7 +2162,7 @@ function addIssueTools(input) {
|
|
|
1636
2162
|
throw new Error("linear_update_issue is unavailable: Linear client does not support updateIssue().");
|
|
1637
2163
|
}
|
|
1638
2164
|
const issueId = resolveIssueId("linear_update_issue", args);
|
|
1639
|
-
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;
|
|
2165
|
+
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.delegate_id !== void 0 || args.estimate !== void 0 || args.due_date !== void 0;
|
|
1640
2166
|
if (!hasUpdates) {
|
|
1641
2167
|
throw new Error("linear_update_issue requires at least one update field.");
|
|
1642
2168
|
}
|
|
@@ -1648,6 +2174,7 @@ function addIssueTools(input) {
|
|
|
1648
2174
|
...args.priority !== void 0 ? { priority: args.priority } : {},
|
|
1649
2175
|
...args.state_id !== void 0 ? { stateId: args.state_id } : {},
|
|
1650
2176
|
...args.assignee_id !== void 0 ? { assigneeId: args.assignee_id } : {},
|
|
2177
|
+
...args.delegate_id !== void 0 ? { delegateId: args.delegate_id } : {},
|
|
1651
2178
|
...args.estimate !== void 0 ? { estimate: args.estimate } : {},
|
|
1652
2179
|
...args.due_date !== void 0 ? { dueDate: args.due_date } : {}
|
|
1653
2180
|
}
|
|
@@ -1678,7 +2205,7 @@ function addIssueTools(input) {
|
|
|
1678
2205
|
function assertUuid(toolName, value) {
|
|
1679
2206
|
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;
|
|
1680
2207
|
if (!uuidPattern.test(value)) {
|
|
1681
|
-
throw new Error(`${toolName} requires the exact Linear
|
|
2208
|
+
throw new Error(`${toolName} requires the exact Linear UUID from the session context (not the human-readable identifier). Received "${value}".`);
|
|
1682
2209
|
}
|
|
1683
2210
|
}
|
|
1684
2211
|
function resolveIssueId(toolName, args) {
|
|
@@ -1721,6 +2248,11 @@ async function readIssue(client, issueId) {
|
|
|
1721
2248
|
id: issue.assignee.id ?? null,
|
|
1722
2249
|
name: issue.assignee.displayName ?? issue.assignee.name ?? null
|
|
1723
2250
|
} : null,
|
|
2251
|
+
delegate: issue.delegate ? {
|
|
2252
|
+
id: issue.delegate.id ?? null,
|
|
2253
|
+
name: issue.delegate.displayName ?? issue.delegate.name ?? null
|
|
2254
|
+
} : null,
|
|
2255
|
+
delegate_id: issue.delegateId ?? null,
|
|
1724
2256
|
team: issue.team ? {
|
|
1725
2257
|
id: issue.team.id ?? null,
|
|
1726
2258
|
key: issue.team.key ?? null,
|
|
@@ -1776,6 +2308,296 @@ async function readWorkflowStates(client, teamId) {
|
|
|
1776
2308
|
})).sort((left, right) => (left.position ?? Number.MAX_SAFE_INTEGER) - (right.position ?? Number.MAX_SAFE_INTEGER));
|
|
1777
2309
|
return { team_id: teamId, states };
|
|
1778
2310
|
}
|
|
2311
|
+
function addSessionCreationTools(input) {
|
|
2312
|
+
const { tools, client, store, logger } = input;
|
|
2313
|
+
const sessionCreationBaseSchema = import_zod.z.object({
|
|
2314
|
+
external_link: import_zod.z.string().url().optional().describe("Optional URL of an external page associated with this session"),
|
|
2315
|
+
room_id: import_zod.z.string().min(1).optional().describe("The Thenvoi room ID to persist the session-room mapping. Pass this when creating a session from within a Thenvoi conversation.")
|
|
2316
|
+
});
|
|
2317
|
+
const sessionCreationSchema = sessionCreationBaseSchema.extend({
|
|
2318
|
+
issue_id: import_zod.z.string().uuid().describe("The Linear issue ID (UUID)")
|
|
2319
|
+
});
|
|
2320
|
+
async function persistSessionRoom(sessionId, issueId, roomId) {
|
|
2321
|
+
if (!store || typeof roomId !== "string") return null;
|
|
2322
|
+
try {
|
|
2323
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2324
|
+
await store.upsert({
|
|
2325
|
+
linearSessionId: sessionId,
|
|
2326
|
+
linearIssueId: issueId,
|
|
2327
|
+
thenvoiRoomId: roomId,
|
|
2328
|
+
status: "active",
|
|
2329
|
+
createdAt: now,
|
|
2330
|
+
updatedAt: now
|
|
2331
|
+
});
|
|
2332
|
+
return null;
|
|
2333
|
+
} catch (err) {
|
|
2334
|
+
logger?.warn("linear_tools.session_room_persist_failed", {
|
|
2335
|
+
sessionId,
|
|
2336
|
+
issueId,
|
|
2337
|
+
roomId,
|
|
2338
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2339
|
+
});
|
|
2340
|
+
return "session-room mapping not persisted";
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
const createOnIssue = client.agentSessionCreateOnIssue?.bind(client);
|
|
2344
|
+
if (typeof createOnIssue === "function") {
|
|
2345
|
+
tools.push({
|
|
2346
|
+
name: "linear_create_session_on_issue",
|
|
2347
|
+
description: "Create a new Linear agent session on an existing issue. Use this when the conversation produces work that should be tracked against a known Linear issue and no session exists yet.",
|
|
2348
|
+
schema: sessionCreationSchema,
|
|
2349
|
+
handler: async (args) => {
|
|
2350
|
+
const issueId = args.issue_id;
|
|
2351
|
+
const result = await createOnIssue({
|
|
2352
|
+
issueId,
|
|
2353
|
+
...typeof args.external_link === "string" ? { externalLink: args.external_link } : {}
|
|
2354
|
+
});
|
|
2355
|
+
const session = extractCreatedSession(result, logger);
|
|
2356
|
+
const warning = await persistSessionRoom(session.id, session.issueId, args.room_id);
|
|
2357
|
+
return { ok: true, session, ...warning ? { warning } : {} };
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
const createOnComment = client.agentSessionCreateOnComment?.bind(client);
|
|
2362
|
+
if (typeof createOnComment === "function") {
|
|
2363
|
+
tools.push({
|
|
2364
|
+
name: "linear_create_session_on_comment",
|
|
2365
|
+
description: "Create a new Linear agent session on a specific comment thread. Use this to attach agent work to an existing discussion on a Linear issue.",
|
|
2366
|
+
schema: sessionCreationBaseSchema.extend({
|
|
2367
|
+
comment_id: import_zod.z.string().uuid().describe("The Linear comment ID (UUID)")
|
|
2368
|
+
}),
|
|
2369
|
+
handler: async (args) => {
|
|
2370
|
+
const commentId = args.comment_id;
|
|
2371
|
+
const result = await createOnComment({
|
|
2372
|
+
commentId,
|
|
2373
|
+
...typeof args.external_link === "string" ? { externalLink: args.external_link } : {}
|
|
2374
|
+
});
|
|
2375
|
+
const session = extractCreatedSession(result, logger);
|
|
2376
|
+
const warning = await persistSessionRoom(session.id, session.issueId, args.room_id);
|
|
2377
|
+
return { ok: true, session, ...warning ? { warning } : {} };
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
const createIssueFn = client.createIssue?.bind(client);
|
|
2382
|
+
if (typeof createIssueFn === "function") {
|
|
2383
|
+
tools.push({
|
|
2384
|
+
name: "linear_create_issue",
|
|
2385
|
+
description: "Create a new Linear issue from scratch. Use this when the Thenvoi conversation produces work that should be tracked as a new Linear issue. After creating the issue, call linear_create_session_on_issue to attach an agent session. Never create issues without explicit human intent or clear delegation.",
|
|
2386
|
+
schema: import_zod.z.object({
|
|
2387
|
+
team_id: import_zod.z.string().uuid().describe("The Linear team ID to create the issue in"),
|
|
2388
|
+
title: import_zod.z.string().min(1).describe("Issue title"),
|
|
2389
|
+
description: import_zod.z.string().optional().describe("Issue description in Markdown"),
|
|
2390
|
+
priority: import_zod.z.number().int().min(0).max(4).optional().describe("Priority 0-4 (0=none, 1=urgent, 2=high, 3=normal, 4=low)"),
|
|
2391
|
+
assignee_id: import_zod.z.string().uuid().optional().describe("Assignee user ID (UUID)"),
|
|
2392
|
+
state_id: import_zod.z.string().uuid().optional().describe("Workflow state ID (UUID)"),
|
|
2393
|
+
label_ids: import_zod.z.array(import_zod.z.string().uuid()).optional().describe("Label IDs to attach (UUIDs)")
|
|
2394
|
+
}),
|
|
2395
|
+
handler: async (args) => {
|
|
2396
|
+
const result = await createIssueFn({
|
|
2397
|
+
teamId: args.team_id,
|
|
2398
|
+
title: args.title,
|
|
2399
|
+
...typeof args.description === "string" ? { description: args.description } : {},
|
|
2400
|
+
...typeof args.priority === "number" ? { priority: args.priority } : {},
|
|
2401
|
+
...typeof args.assignee_id === "string" ? { assigneeId: args.assignee_id } : {},
|
|
2402
|
+
...typeof args.state_id === "string" ? { stateId: args.state_id } : {},
|
|
2403
|
+
...Array.isArray(args.label_ids) ? { labelIds: args.label_ids } : {}
|
|
2404
|
+
});
|
|
2405
|
+
const issue = extractCreatedIssue(result, logger);
|
|
2406
|
+
return { ok: true, issue };
|
|
2407
|
+
}
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
function extractCreatedSession(result, logger) {
|
|
2412
|
+
const payload = typeof result === "object" && result !== null ? result : {};
|
|
2413
|
+
const session = typeof payload.agentSession === "object" && payload.agentSession !== null ? payload.agentSession : payload;
|
|
2414
|
+
const id = typeof session.id === "string" ? session.id : null;
|
|
2415
|
+
if (!id) {
|
|
2416
|
+
logger?.error("linear_tools.orphan_session_created", {
|
|
2417
|
+
raw: safeStringify(result)
|
|
2418
|
+
});
|
|
2419
|
+
throw new Error("Linear API returned a session without an ID.");
|
|
2420
|
+
}
|
|
2421
|
+
return {
|
|
2422
|
+
id,
|
|
2423
|
+
issueId: typeof session.issueId === "string" ? session.issueId : null,
|
|
2424
|
+
status: typeof session.status === "string" ? session.status : null
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
function extractCreatedIssue(result, logger) {
|
|
2428
|
+
const payload = typeof result === "object" && result !== null ? result : {};
|
|
2429
|
+
const issue = typeof payload.issue === "object" && payload.issue !== null ? payload.issue : payload;
|
|
2430
|
+
const id = typeof issue.id === "string" ? issue.id : null;
|
|
2431
|
+
if (!id) {
|
|
2432
|
+
logger?.error("linear_tools.orphan_issue_created", {
|
|
2433
|
+
raw: safeStringify(result)
|
|
2434
|
+
});
|
|
2435
|
+
throw new Error("Linear API returned an issue without an ID.");
|
|
2436
|
+
}
|
|
2437
|
+
return {
|
|
2438
|
+
id,
|
|
2439
|
+
identifier: typeof issue.identifier === "string" ? issue.identifier : null,
|
|
2440
|
+
url: typeof issue.url === "string" ? issue.url : null,
|
|
2441
|
+
title: typeof issue.title === "string" ? issue.title : null
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
function safeStringify(value) {
|
|
2445
|
+
try {
|
|
2446
|
+
return JSON.stringify(value);
|
|
2447
|
+
} catch {
|
|
2448
|
+
return String(value);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
function extractRepositorySuggestions(response) {
|
|
2452
|
+
if (response == null || typeof response !== "object") {
|
|
2453
|
+
return [];
|
|
2454
|
+
}
|
|
2455
|
+
const payload = response;
|
|
2456
|
+
if (!Array.isArray(payload.suggestions)) {
|
|
2457
|
+
return [];
|
|
2458
|
+
}
|
|
2459
|
+
return payload.suggestions.filter(
|
|
2460
|
+
(entry) => entry != null && typeof entry === "object" && typeof entry.repositoryFullName === "string"
|
|
2461
|
+
).map((entry) => ({
|
|
2462
|
+
repositoryFullName: entry.repositoryFullName,
|
|
2463
|
+
hostname: typeof entry.hostname === "string" ? entry.hostname : null,
|
|
2464
|
+
confidence: typeof entry.confidence === "number" ? entry.confidence : 0
|
|
2465
|
+
})).sort((a, b) => b.confidence - a.confidence);
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// src/integrations/linear/notification.ts
|
|
2469
|
+
var MAX_COMMENT_LENGTH = 4e3;
|
|
2470
|
+
var MAX_ACTOR_NAME_LENGTH = 100;
|
|
2471
|
+
function sanitizeInlineText(text) {
|
|
2472
|
+
return text.replace(/[\x00-\x1f\x7f]/g, " ").trim();
|
|
2473
|
+
}
|
|
2474
|
+
function sanitizeBodyText(text) {
|
|
2475
|
+
return text.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "").trim();
|
|
2476
|
+
}
|
|
2477
|
+
async function handleAppUserNotification(input) {
|
|
2478
|
+
const { payload, deps, logger, appUserId } = input;
|
|
2479
|
+
const notification = payload.notification;
|
|
2480
|
+
const typename = notification.__typename;
|
|
2481
|
+
switch (typename) {
|
|
2482
|
+
case "IssueUnassignedFromYouNotificationWebhookPayload":
|
|
2483
|
+
await handleIssueUnassigned({
|
|
2484
|
+
notification,
|
|
2485
|
+
deps,
|
|
2486
|
+
logger
|
|
2487
|
+
});
|
|
2488
|
+
return;
|
|
2489
|
+
case "IssueNewCommentNotificationWebhookPayload":
|
|
2490
|
+
await handleIssueNewComment({
|
|
2491
|
+
notification,
|
|
2492
|
+
deps,
|
|
2493
|
+
logger,
|
|
2494
|
+
appUserId
|
|
2495
|
+
});
|
|
2496
|
+
return;
|
|
2497
|
+
case "IssueCommentReactionNotificationWebhookPayload":
|
|
2498
|
+
case "IssueEmojiReactionNotificationWebhookPayload":
|
|
2499
|
+
logReaction({
|
|
2500
|
+
notification,
|
|
2501
|
+
logger
|
|
2502
|
+
});
|
|
2503
|
+
return;
|
|
2504
|
+
default:
|
|
2505
|
+
logger.info("linear_thenvoi_bridge.notification_unhandled", {
|
|
2506
|
+
notificationType: typename
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
async function handleIssueUnassigned(input) {
|
|
2511
|
+
const { notification, deps, logger } = input;
|
|
2512
|
+
const existing = await deps.store.getByIssueId(notification.issueId);
|
|
2513
|
+
if (!existing || existing.status !== "active" && existing.status !== "waiting") {
|
|
2514
|
+
logger.info("linear_thenvoi_bridge.notification_unassigned_no_session", {
|
|
2515
|
+
issueId: notification.issueId,
|
|
2516
|
+
reason: existing ? `session_status_${existing.status}` : "no_active_session"
|
|
2517
|
+
});
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
await deps.thenvoiRest.createChatEvent(existing.thenvoiRoomId, {
|
|
2521
|
+
content: "[Linear]: Issue unassigned from agent. Disengage from active work and await reassignment.",
|
|
2522
|
+
messageType: "task",
|
|
2523
|
+
metadata: {
|
|
2524
|
+
linear_notification_type: "issueUnassignedFromYou",
|
|
2525
|
+
linear_issue_id: notification.issueId,
|
|
2526
|
+
linear_actor_id: notification.actorId ?? null,
|
|
2527
|
+
linear_bridge: "thenvoi"
|
|
2528
|
+
}
|
|
2529
|
+
});
|
|
2530
|
+
await deps.store.markCanceled(existing.linearSessionId);
|
|
2531
|
+
logger.info("linear_thenvoi_bridge.notification_unassigned_handled", {
|
|
2532
|
+
issueId: notification.issueId,
|
|
2533
|
+
sessionId: existing.linearSessionId,
|
|
2534
|
+
roomId: existing.thenvoiRoomId
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
async function handleIssueNewComment(input) {
|
|
2538
|
+
const { notification, deps, logger, appUserId } = input;
|
|
2539
|
+
if (appUserId && notification.actorId === appUserId) {
|
|
2540
|
+
logger.info("linear_thenvoi_bridge.notification_comment_self_skipped", {
|
|
2541
|
+
issueId: notification.issueId,
|
|
2542
|
+
commentId: notification.commentId,
|
|
2543
|
+
actorId: notification.actorId
|
|
2544
|
+
});
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
const existing = await deps.store.getByIssueId(notification.issueId);
|
|
2548
|
+
if (!existing || existing.status !== "active" && existing.status !== "waiting") {
|
|
2549
|
+
logger.info("linear_thenvoi_bridge.notification_comment_skipped", {
|
|
2550
|
+
issueId: notification.issueId,
|
|
2551
|
+
reason: existing ? `session_status_${existing.status}` : "no_active_session"
|
|
2552
|
+
});
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
let actorName = sanitizeInlineText(notification.actor?.name ?? "");
|
|
2556
|
+
if (actorName.length > MAX_ACTOR_NAME_LENGTH) {
|
|
2557
|
+
actorName = actorName.slice(0, MAX_ACTOR_NAME_LENGTH) + "\u2026";
|
|
2558
|
+
}
|
|
2559
|
+
if (actorName.length === 0) {
|
|
2560
|
+
actorName = "Unknown user";
|
|
2561
|
+
}
|
|
2562
|
+
let commentBody = sanitizeBodyText(notification.comment.body ?? "");
|
|
2563
|
+
if (commentBody.length > MAX_COMMENT_LENGTH) {
|
|
2564
|
+
commentBody = commentBody.slice(0, MAX_COMMENT_LENGTH) + "\u2026";
|
|
2565
|
+
}
|
|
2566
|
+
if (!notification.actor?.name) {
|
|
2567
|
+
logger.info("linear_thenvoi_bridge.notification_comment_actor_fallback", {
|
|
2568
|
+
issueId: notification.issueId,
|
|
2569
|
+
commentId: notification.commentId,
|
|
2570
|
+
actorId: notification.actorId ?? null
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2573
|
+
await deps.thenvoiRest.createChatEvent(existing.thenvoiRoomId, {
|
|
2574
|
+
content: `[Linear Comment from ${actorName}]: ${commentBody}`,
|
|
2575
|
+
messageType: "text",
|
|
2576
|
+
metadata: {
|
|
2577
|
+
linear_notification_type: "issueNewComment",
|
|
2578
|
+
linear_issue_id: notification.issueId,
|
|
2579
|
+
linear_comment_id: notification.commentId,
|
|
2580
|
+
linear_actor_id: notification.actorId ?? null,
|
|
2581
|
+
linear_bridge: "thenvoi"
|
|
2582
|
+
}
|
|
2583
|
+
});
|
|
2584
|
+
logger.info("linear_thenvoi_bridge.notification_comment_forwarded", {
|
|
2585
|
+
issueId: notification.issueId,
|
|
2586
|
+
commentId: notification.commentId,
|
|
2587
|
+
sessionId: existing.linearSessionId,
|
|
2588
|
+
roomId: existing.thenvoiRoomId
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
function logReaction(input) {
|
|
2592
|
+
const { notification, logger } = input;
|
|
2593
|
+
logger.info("linear_thenvoi_bridge.notification_reaction", {
|
|
2594
|
+
notificationType: notification.__typename,
|
|
2595
|
+
issueId: notification.issueId,
|
|
2596
|
+
actorId: notification.actorId ?? null,
|
|
2597
|
+
commentId: "commentId" in notification ? notification.commentId : null,
|
|
2598
|
+
reactionEmoji: notification.reactionEmoji
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
1779
2601
|
|
|
1780
2602
|
// src/integrations/linear/webhook.ts
|
|
1781
2603
|
var import_webhooks = require("@linear/sdk/webhooks");
|
|
@@ -1911,6 +2733,7 @@ function createLinearWebhookHandler(options) {
|
|
|
1911
2733
|
const dispatcher = options.dispatcher ?? createInlineLinearBridgeDispatcher({ logger });
|
|
1912
2734
|
const webhookClient = new import_webhooks.LinearWebhookClient(options.config.linearWebhookSecret);
|
|
1913
2735
|
const inFlightEventKeys = /* @__PURE__ */ new Set();
|
|
2736
|
+
const processedNotificationIds = /* @__PURE__ */ new Set();
|
|
1914
2737
|
return async (request, response) => {
|
|
1915
2738
|
if (request.method !== "POST") {
|
|
1916
2739
|
logger.warn("linear_thenvoi_bridge.webhook_invalid_method", {
|
|
@@ -1932,9 +2755,9 @@ function createLinearWebhookHandler(options) {
|
|
|
1932
2755
|
return;
|
|
1933
2756
|
}
|
|
1934
2757
|
const rawBody = await readRawBody(request);
|
|
1935
|
-
let
|
|
2758
|
+
let parsed;
|
|
1936
2759
|
try {
|
|
1937
|
-
|
|
2760
|
+
parsed = webhookClient.parseData(
|
|
1938
2761
|
rawBody,
|
|
1939
2762
|
signature,
|
|
1940
2763
|
timestamp
|
|
@@ -1946,13 +2769,111 @@ function createLinearWebhookHandler(options) {
|
|
|
1946
2769
|
sendText(response, 400, "Invalid webhook");
|
|
1947
2770
|
return;
|
|
1948
2771
|
}
|
|
1949
|
-
if (
|
|
2772
|
+
if (parsed.type === "AppUserNotification") {
|
|
2773
|
+
const notificationPayload = parsed;
|
|
2774
|
+
if (!notificationPayload.notification) {
|
|
2775
|
+
logger.warn("linear_thenvoi_bridge.webhook_notification_missing_payload", {
|
|
2776
|
+
appUserId: notificationPayload.appUserId ?? null,
|
|
2777
|
+
organizationId: notificationPayload.organizationId ?? null
|
|
2778
|
+
});
|
|
2779
|
+
sendText(response, 200, "OK");
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
const notificationId = notificationPayload.notification?.id;
|
|
2783
|
+
const notificationType = notificationPayload.notification.__typename;
|
|
2784
|
+
const isDedupable = notificationType !== "IssueCommentReactionNotificationWebhookPayload" && notificationType !== "IssueEmojiReactionNotificationWebhookPayload";
|
|
2785
|
+
if (isDedupable && notificationId && processedNotificationIds.has(notificationId)) {
|
|
2786
|
+
logger.info("linear_thenvoi_bridge.webhook_notification_duplicate_ignored", {
|
|
2787
|
+
notificationId
|
|
2788
|
+
});
|
|
2789
|
+
sendText(response, 200, "OK");
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
logger.info("linear_thenvoi_bridge.webhook_notification_received", {
|
|
2793
|
+
notificationType: notificationType ?? null,
|
|
2794
|
+
notificationId: notificationId ?? null,
|
|
2795
|
+
appUserId: notificationPayload.appUserId,
|
|
2796
|
+
organizationId: notificationPayload.organizationId
|
|
2797
|
+
});
|
|
2798
|
+
try {
|
|
2799
|
+
await handleAppUserNotification({
|
|
2800
|
+
payload: notificationPayload,
|
|
2801
|
+
deps: options.deps,
|
|
2802
|
+
logger,
|
|
2803
|
+
appUserId: notificationPayload.appUserId
|
|
2804
|
+
});
|
|
2805
|
+
if (isDedupable && notificationId) {
|
|
2806
|
+
processedNotificationIds.add(notificationId);
|
|
2807
|
+
if (processedNotificationIds.size > 1e3) {
|
|
2808
|
+
const oldest = processedNotificationIds.values().next().value;
|
|
2809
|
+
if (oldest) processedNotificationIds.delete(oldest);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
} catch (error) {
|
|
2813
|
+
logger.error("linear_thenvoi_bridge.webhook_notification_failed", {
|
|
2814
|
+
notificationType: notificationType ?? null,
|
|
2815
|
+
error: serializeError(error)
|
|
2816
|
+
});
|
|
2817
|
+
}
|
|
2818
|
+
sendText(response, 200, "OK");
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
if (parsed.type === "PermissionChange") {
|
|
2822
|
+
const action = parsed.action;
|
|
2823
|
+
if (normalizeAction2(action) === "teamaccesschanged") {
|
|
2824
|
+
const permPayload = parsed;
|
|
2825
|
+
logger.info("linear_thenvoi_bridge.team_access_changed", {
|
|
2826
|
+
action: permPayload.action,
|
|
2827
|
+
addedTeamIds: permPayload.addedTeamIds,
|
|
2828
|
+
removedTeamIds: permPayload.removedTeamIds,
|
|
2829
|
+
canAccessAllPublicTeams: permPayload.canAccessAllPublicTeams,
|
|
2830
|
+
organizationId: permPayload.organizationId
|
|
2831
|
+
});
|
|
2832
|
+
try {
|
|
2833
|
+
await options.permissionCallbacks?.onTeamAccessChanged?.(permPayload);
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
logger.error("linear_thenvoi_bridge.team_access_changed_callback_failed", {
|
|
2836
|
+
error: serializeError(error)
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2839
|
+
} else {
|
|
2840
|
+
logger.info("linear_thenvoi_bridge.permission_change_event_ignored", {
|
|
2841
|
+
action
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
sendText(response, 200, "OK");
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
if (parsed.type === "OAuthApp") {
|
|
2848
|
+
const oauthPayload = parsed;
|
|
2849
|
+
if (normalizeAction2(oauthPayload.action) === "revoked") {
|
|
2850
|
+
logger.warn("linear_thenvoi_bridge.oauth_app_revoked", {
|
|
2851
|
+
oauthClientId: oauthPayload.oauthClientId,
|
|
2852
|
+
organizationId: oauthPayload.organizationId
|
|
2853
|
+
});
|
|
2854
|
+
try {
|
|
2855
|
+
await options.permissionCallbacks?.onOAuthAppRevoked?.(oauthPayload);
|
|
2856
|
+
} catch (error) {
|
|
2857
|
+
logger.error("linear_thenvoi_bridge.oauth_app_revoked_callback_failed", {
|
|
2858
|
+
error: serializeError(error)
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
} else {
|
|
2862
|
+
logger.info("linear_thenvoi_bridge.oauth_app_event_ignored", {
|
|
2863
|
+
action: oauthPayload.action
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
sendText(response, 200, "OK");
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
if (parsed.type !== "AgentSessionEvent") {
|
|
1950
2870
|
logger.info("linear_thenvoi_bridge.webhook_ignored_event", {
|
|
1951
|
-
type:
|
|
2871
|
+
type: parsed.type
|
|
1952
2872
|
});
|
|
1953
2873
|
sendText(response, 200, "OK");
|
|
1954
2874
|
return;
|
|
1955
2875
|
}
|
|
2876
|
+
const payload = parsed;
|
|
1956
2877
|
const eventKey = getAgentSessionEventKey(payload);
|
|
1957
2878
|
logger.info("linear_thenvoi_bridge.webhook_received", {
|
|
1958
2879
|
sessionId: payload.agentSession.id,
|
|
@@ -2185,6 +3106,12 @@ var DEFAULT_STATUS_MAPPING = {
|
|
|
2185
3106
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2186
3107
|
0 && (module.exports = {
|
|
2187
3108
|
DEFAULT_STATUS_MAPPING,
|
|
3109
|
+
ELICITATION_BODY_MAX_LENGTH,
|
|
3110
|
+
PROVIDER_MAX_LENGTH,
|
|
3111
|
+
SELECT_OPTION_MAX_LENGTH,
|
|
3112
|
+
STALE_SESSION_CHECK_INTERVAL_MS,
|
|
3113
|
+
STALE_SESSION_THRESHOLD_MS,
|
|
3114
|
+
StaleSessionGuard,
|
|
2188
3115
|
buildLinearAuthorizationHeader,
|
|
2189
3116
|
completeLinearSession,
|
|
2190
3117
|
createInProcessLinearBridgeDispatcher,
|
|
@@ -2196,12 +3123,17 @@ var DEFAULT_STATUS_MAPPING = {
|
|
|
2196
3123
|
createSqliteSessionRoomStore,
|
|
2197
3124
|
dedupeHandles,
|
|
2198
3125
|
handleAgentSessionEvent,
|
|
3126
|
+
handleAppUserNotification,
|
|
2199
3127
|
isLinearApiKey,
|
|
3128
|
+
isSessionStale,
|
|
2200
3129
|
postAction,
|
|
3130
|
+
postAuthElicitation,
|
|
2201
3131
|
postElicitation,
|
|
2202
3132
|
postError,
|
|
2203
3133
|
postResponse,
|
|
3134
|
+
postSelectElicitation,
|
|
2204
3135
|
postThought,
|
|
3136
|
+
sendRecoveryActivityIfStale,
|
|
2205
3137
|
stripHandlePrefix,
|
|
2206
3138
|
updatePlan
|
|
2207
3139
|
});
|