@band-ai/sdk 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +372 -0
- package/dist/{ClaudeSDKAdapter-CRq4R-WB.d.cts → ClaudeSDKAdapter-BY4zEbCl.d.cts} +14 -10
- package/dist/{ClaudeSDKAdapter-BHOkjN3X.d.ts → ClaudeSDKAdapter-Cna5P1dR.d.ts} +14 -10
- package/dist/{acp-server-B1Da7L6u.d.ts → acp-server-CiUqN3G3.d.ts} +2 -2
- package/dist/{acp-server-BdRz9_UN.d.cts → acp-server-Dlj7D563.d.cts} +2 -2
- package/dist/adapters.cjs +734 -279
- package/dist/adapters.d.cts +21 -19
- package/dist/adapters.d.ts +21 -19
- package/dist/adapters.js +65 -44
- package/dist/backends-D-dBdDm-.d.ts +27 -0
- package/dist/backends-ZLxmQrTZ.d.cts +27 -0
- package/dist/{chunk-MSL6CWHY.js → chunk-4IAXZBXX.js} +77 -37
- package/dist/{chunk-SG4WLD2H.js → chunk-EAI2M3WM.js} +38 -94
- package/dist/{chunk-YR3BOQNW.js → chunk-JY75VSQK.js} +7 -7
- package/dist/{chunk-INA6YZTZ.js → chunk-LBWKISBS.js} +51 -31
- package/dist/{chunk-I5EZG52P.js → chunk-N2QTUMGO.js} +532 -327
- package/dist/{chunk-RHVH2GZR.js → chunk-NMZLX7TP.js} +110 -56
- package/dist/{chunk-EDPKUUJL.js → chunk-NT7TBFX2.js} +9 -1
- package/dist/{chunk-GVEQ3RAH.js → chunk-RV76SKFT.js} +169 -54
- package/dist/{chunk-W5NDZSYE.js → chunk-TLBCKDQO.js} +23 -23
- package/dist/chunk-US226AS4.js +130 -0
- package/dist/{chunk-IZPV6QS6.js → chunk-WL2HNJZ6.js} +38 -11
- package/dist/chunk-WLCM2OXG.js +181 -0
- package/dist/config.cjs +40 -13
- package/dist/config.d.cts +7 -1
- package/dist/config.d.ts +7 -1
- package/dist/config.js +2 -2
- package/dist/converters.cjs +491 -35
- package/dist/converters.d.cts +5 -5
- package/dist/converters.d.ts +5 -5
- package/dist/converters.js +4 -3
- package/dist/core.cjs +53 -10
- package/dist/core.d.cts +10 -9
- package/dist/core.d.ts +10 -9
- package/dist/core.js +8 -4
- package/dist/disconnectReason-IuADl6TH.d.cts +34 -0
- package/dist/disconnectReason-IuADl6TH.d.ts +34 -0
- package/dist/{dtos-DUpbIu8k.d.ts → dtos-B8KU_q4d.d.cts} +26 -6
- package/dist/{dtos-DUpbIu8k.d.cts → dtos-B8KU_q4d.d.ts} +26 -6
- package/dist/index.cjs +1510 -735
- package/dist/index.d.cts +18 -16
- package/dist/index.d.ts +18 -16
- package/dist/index.js +21 -13
- package/dist/linear.cjs +1044 -112
- package/dist/linear.d.cts +146 -16
- package/dist/linear.d.ts +146 -16
- package/dist/linear.js +1031 -110
- package/dist/mcp-claude.cjs +338 -66
- package/dist/mcp-claude.d.cts +3 -3
- package/dist/mcp-claude.d.ts +3 -3
- package/dist/mcp-claude.js +215 -16
- package/dist/mcp.cjs +414 -105
- package/dist/mcp.d.cts +29 -16
- package/dist/mcp.d.ts +29 -16
- package/dist/mcp.js +14 -14
- package/dist/{opencode-BBcDchcN.d.cts → opencode-2YdscIc1.d.cts} +5 -5
- package/dist/{opencode-DthQQUsZ.d.ts → opencode-Bld62x5t.d.ts} +5 -5
- package/dist/{pagination-CcDkVFxK.d.ts → pagination-BgEx-HeL.d.ts} +2 -2
- package/dist/{pagination-BPiys10t.d.cts → pagination-DqdkQArT.d.cts} +2 -2
- package/dist/{protocols-DrhzOH6K.d.ts → protocols-Bwwcql0h.d.ts} +1 -1
- package/dist/{protocols-CInhtFpA.d.cts → protocols-Bz1rxcUg.d.cts} +1 -1
- package/dist/rest.cjs +41 -97
- package/dist/rest.d.cts +7 -7
- package/dist/rest.d.ts +7 -7
- package/dist/rest.js +2 -2
- package/dist/runtime.cjs +998 -620
- package/dist/runtime.d.cts +22 -29
- package/dist/runtime.d.ts +22 -29
- package/dist/runtime.js +19 -67
- package/dist/{schemas-BpEBkq5V.d.cts → schemas-BotL3ZLH.d.cts} +51 -56
- package/dist/{schemas-vos1AaBc.d.ts → schemas-DBsC4J1g.d.ts} +51 -56
- package/dist/{sdk-CwTAxhwI.d.cts → sdk-BTciSLRQ.d.cts} +35 -5
- package/dist/{sdk-CcnQ5OUC.d.ts → sdk-qKJtMma_.d.ts} +35 -5
- package/dist/{simpleAdapter-9OE_p6QW.d.ts → simpleAdapter--wznuoOw.d.ts} +1 -1
- package/dist/{simpleAdapter-DqhvjsGr.d.cts → simpleAdapter-BpT4XbZC.d.cts} +1 -1
- package/dist/testing.d.cts +3 -3
- package/dist/testing.d.ts +3 -3
- package/dist/{types-7jVzOeOP.d.ts → types-BTN_ZETM.d.ts} +2 -2
- package/dist/{types-CxII3XkA.d.ts → types-BgYFd_Yw.d.cts} +5 -5
- package/dist/{types-CSHFipIc.d.cts → types-BivGO7I9.d.ts} +5 -5
- package/dist/{types-CovcHpYf.d.cts → types-D958TC71.d.cts} +2 -2
- package/package.json +25 -23
- package/dist/backends-CzDbiQar.d.ts +0 -20
- package/dist/backends-SVMT-7l4.d.cts +0 -20
- package/dist/chunk-SFVKOEQH.js +0 -103
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
|
});
|
|
@@ -57,13 +68,13 @@ module.exports = __toCommonJS(linear_exports);
|
|
|
57
68
|
var import_sdk2 = require("@linear/sdk");
|
|
58
69
|
|
|
59
70
|
// src/core/errors.ts
|
|
60
|
-
var
|
|
71
|
+
var BandSdkError = class extends Error {
|
|
61
72
|
constructor(message, cause) {
|
|
62
73
|
super(message, cause !== void 0 ? { cause } : void 0);
|
|
63
|
-
this.name = "
|
|
74
|
+
this.name = "BandSdkError";
|
|
64
75
|
}
|
|
65
76
|
};
|
|
66
|
-
var UnsupportedFeatureError = class extends
|
|
77
|
+
var UnsupportedFeatureError = class extends BandSdkError {
|
|
67
78
|
constructor(message) {
|
|
68
79
|
super(message);
|
|
69
80
|
this.name = "UnsupportedFeatureError";
|
|
@@ -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}
|
|
@@ -208,7 +401,7 @@ Bridge responsibilities:
|
|
|
208
401
|
- keep Linear updated with meaningful milestones
|
|
209
402
|
- use linear_post_thought and linear_post_action for meaningful progress updates
|
|
210
403
|
- call linear_post_response when the work is actually finished
|
|
211
|
-
- when you delegate inside
|
|
404
|
+
- when you delegate inside Band, include the relevant ticket context in the room message so the specialist can act without hidden state
|
|
212
405
|
|
|
213
406
|
Suggested peers available in the registry right now. They are not in the room yet:
|
|
214
407
|
${suggestedPeersLine}
|
|
@@ -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_APP_BASE_URL = "https://app.band.ai";
|
|
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,17 +589,59 @@ 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
|
+
bandRest: input.deps.bandRest,
|
|
386
638
|
configuredHostHandle: config.hostAgentHandle,
|
|
387
639
|
logger,
|
|
388
640
|
runtime
|
|
389
641
|
});
|
|
390
642
|
const existingByIssue = config.roomStrategy === "issue" && issueId ? await input.deps.store.getByIssueId(issueId) : null;
|
|
391
643
|
roomRecord = await resolveRoomRecord({
|
|
392
|
-
|
|
644
|
+
bandRest: input.deps.bandRest,
|
|
393
645
|
store: input.deps.store,
|
|
394
646
|
roomStrategy: config.roomStrategy,
|
|
395
647
|
sessionId,
|
|
@@ -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.bandRoomId;
|
|
654
|
+
externalUrlPromise = trySetSessionExternalUrl({
|
|
655
|
+
linearClient: input.deps.linearClient,
|
|
656
|
+
sessionId,
|
|
657
|
+
roomId,
|
|
658
|
+
appBaseUrl: config.bandAppBaseUrl,
|
|
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,
|
|
@@ -405,8 +673,8 @@ async function handleAgentSessionEvent(input, options) {
|
|
|
405
673
|
issueDescription: input.payload.agentSession.issue?.description
|
|
406
674
|
});
|
|
407
675
|
const suggestedPeerHandles = await selectRelevantPeerHandles({
|
|
408
|
-
|
|
409
|
-
roomId: roomRecord.
|
|
676
|
+
bandRest: input.deps.bandRest,
|
|
677
|
+
roomId: roomRecord.bandRoomId,
|
|
410
678
|
intent: sessionIntent,
|
|
411
679
|
hostAgentHandle,
|
|
412
680
|
planningAgentHandles: input.config.planningAgentHandles,
|
|
@@ -414,11 +682,27 @@ async function handleAgentSessionEvent(input, options) {
|
|
|
414
682
|
logger
|
|
415
683
|
});
|
|
416
684
|
await ensureRoomParticipants({
|
|
417
|
-
|
|
418
|
-
roomId: roomRecord.
|
|
685
|
+
bandRest: input.deps.bandRest,
|
|
686
|
+
roomId: roomRecord.bandRoomId,
|
|
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,
|
|
@@ -458,17 +744,17 @@ async function handleAgentSessionEvent(input, options) {
|
|
|
458
744
|
linear_issue_id: issueId,
|
|
459
745
|
linear_prompt_context: input.payload.promptContext ?? null,
|
|
460
746
|
linear_writeback_mode: config.writebackMode,
|
|
461
|
-
linear_bridge: "
|
|
747
|
+
linear_bridge: "band",
|
|
462
748
|
linear_host_handle: hostAgentHandle
|
|
463
749
|
};
|
|
464
750
|
const shouldResetRoomSession = Boolean(
|
|
465
|
-
existingByIssue && existingByIssue.linearSessionId !== sessionId && existingByIssue.
|
|
751
|
+
existingByIssue && existingByIssue.linearSessionId !== sessionId && existingByIssue.bandRoomId === roomRecord.bandRoomId && (existingByIssue.status === "completed" || existingByIssue.status === "errored")
|
|
466
752
|
);
|
|
467
|
-
const authenticatedHostHandle = runtime.authenticatedHostHandleCache.get(input.deps.
|
|
753
|
+
const authenticatedHostHandle = runtime.authenticatedHostHandleCache.get(input.deps.bandRest) ?? null;
|
|
468
754
|
const canBootstrapDirectly = options?.skipRoomWrite === true || authenticatedHostHandle === null || authenticatedHostHandle === hostAgentHandle;
|
|
469
755
|
if (!options?.skipRoomWrite || !canBootstrapDirectly) {
|
|
470
756
|
roomRecord = await forwardBridgeMessage({
|
|
471
|
-
|
|
757
|
+
bandRest: input.deps.bandRest,
|
|
472
758
|
store: input.deps.store,
|
|
473
759
|
logger,
|
|
474
760
|
roomRecord,
|
|
@@ -486,7 +772,7 @@ async function handleAgentSessionEvent(input, options) {
|
|
|
486
772
|
await enqueueBootstrapRequest(input.deps.store, {
|
|
487
773
|
eventKey,
|
|
488
774
|
linearSessionId: sessionId,
|
|
489
|
-
|
|
775
|
+
bandRoomId: roomRecord.bandRoomId,
|
|
490
776
|
expectedContent: message,
|
|
491
777
|
messageType: "task",
|
|
492
778
|
senderId: linearActor.id,
|
|
@@ -497,19 +783,25 @@ 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,
|
|
509
|
-
roomId: roomRecord.
|
|
798
|
+
roomId: roomRecord.bandRoomId,
|
|
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 Band", 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
|
+
bandAppBaseUrl: config.bandAppBaseUrl ?? DEFAULT_APP_BASE_URL
|
|
570
883
|
};
|
|
571
884
|
}
|
|
572
885
|
function normalizeOptionalHandle(handle) {
|
|
@@ -576,25 +889,82 @@ 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
|
-
input.runtime.resolvedHostHandleCache.set(input.
|
|
951
|
+
input.runtime.resolvedHostHandleCache.set(input.bandRest, input.configuredHostHandle);
|
|
582
952
|
return input.configuredHostHandle;
|
|
583
953
|
}
|
|
584
|
-
const cachedAuthenticated = input.runtime.authenticatedHostHandleCache.get(input.
|
|
954
|
+
const cachedAuthenticated = input.runtime.authenticatedHostHandleCache.get(input.bandRest);
|
|
585
955
|
if (cachedAuthenticated) {
|
|
586
|
-
input.runtime.resolvedHostHandleCache.set(input.
|
|
956
|
+
input.runtime.resolvedHostHandleCache.set(input.bandRest, cachedAuthenticated);
|
|
587
957
|
return cachedAuthenticated;
|
|
588
958
|
}
|
|
589
|
-
const cached = input.runtime.resolvedHostHandleCache.get(input.
|
|
959
|
+
const cached = input.runtime.resolvedHostHandleCache.get(input.bandRest);
|
|
590
960
|
if (cached) {
|
|
591
961
|
return cached;
|
|
592
962
|
}
|
|
593
|
-
const identity = await input.
|
|
963
|
+
const identity = await input.bandRest.getAgentMe();
|
|
594
964
|
const authenticatedAgentHandle = normalizeOptionalHandle(identity.handle);
|
|
595
965
|
if (authenticatedAgentHandle) {
|
|
596
|
-
input.runtime.authenticatedHostHandleCache.set(input.
|
|
597
|
-
input.runtime.resolvedHostHandleCache.set(input.
|
|
966
|
+
input.runtime.authenticatedHostHandleCache.set(input.bandRest, authenticatedAgentHandle);
|
|
967
|
+
input.runtime.resolvedHostHandleCache.set(input.bandRest, authenticatedAgentHandle);
|
|
598
968
|
return authenticatedAgentHandle;
|
|
599
969
|
}
|
|
600
970
|
throw new Error(
|
|
@@ -628,14 +998,14 @@ async function handleCanceledAction(input) {
|
|
|
628
998
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
629
999
|
});
|
|
630
1000
|
if (!input.skipRoomWrite) {
|
|
631
|
-
await input.deps.
|
|
1001
|
+
await input.deps.bandRest.createChatEvent(existing.bandRoomId, {
|
|
632
1002
|
content: "[Linear]: Agent session canceled. Stop in-room execution and await new instructions.",
|
|
633
1003
|
messageType: "task",
|
|
634
1004
|
metadata: {
|
|
635
1005
|
linear_event_action: "canceled",
|
|
636
1006
|
linear_session_id: input.sessionId,
|
|
637
1007
|
linear_issue_id: input.issueId,
|
|
638
|
-
linear_bridge: "
|
|
1008
|
+
linear_bridge: "band"
|
|
639
1009
|
}
|
|
640
1010
|
});
|
|
641
1011
|
}
|
|
@@ -661,15 +1031,21 @@ 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",
|
|
667
1043
|
linear_session_id: input.sessionId,
|
|
668
|
-
linear_bridge: "
|
|
1044
|
+
linear_bridge: "band"
|
|
669
1045
|
};
|
|
670
|
-
const canBootstrapDirectly = input.config.hostAgentHandle !== null ? true : Boolean(normalizeOptionalHandle((await input.deps.
|
|
1046
|
+
const canBootstrapDirectly = input.config.hostAgentHandle !== null ? true : Boolean(normalizeOptionalHandle((await input.deps.bandRest.getAgentMe()).handle));
|
|
671
1047
|
if (!input.skipRoomWrite || !canBootstrapDirectly) {
|
|
672
|
-
await input.deps.
|
|
1048
|
+
await input.deps.bandRest.createChatEvent(existing.bandRoomId, {
|
|
673
1049
|
content: message,
|
|
674
1050
|
messageType: "text",
|
|
675
1051
|
metadata
|
|
@@ -680,7 +1056,7 @@ async function handlePromptedAction(input) {
|
|
|
680
1056
|
await enqueueBootstrapRequest(input.deps.store, {
|
|
681
1057
|
eventKey: input.eventKey,
|
|
682
1058
|
linearSessionId: input.sessionId,
|
|
683
|
-
|
|
1059
|
+
bandRoomId: existing.bandRoomId,
|
|
684
1060
|
expectedContent: message,
|
|
685
1061
|
messageType: "text",
|
|
686
1062
|
senderId: linearActor.id,
|
|
@@ -696,7 +1072,7 @@ async function handlePromptedAction(input) {
|
|
|
696
1072
|
});
|
|
697
1073
|
input.logger.info("linear_thenvoi_bridge.prompted_forwarded", {
|
|
698
1074
|
sessionId: input.sessionId,
|
|
699
|
-
roomId: existing.
|
|
1075
|
+
roomId: existing.bandRoomId
|
|
700
1076
|
});
|
|
701
1077
|
}
|
|
702
1078
|
async function enqueueBootstrapRequest(store, request) {
|
|
@@ -738,7 +1114,7 @@ async function resolveRoomRecord(input) {
|
|
|
738
1114
|
}
|
|
739
1115
|
async function forwardBridgeMessage(input) {
|
|
740
1116
|
try {
|
|
741
|
-
await input.
|
|
1117
|
+
await input.bandRest.createChatEvent(input.roomRecord.bandRoomId, {
|
|
742
1118
|
content: input.message,
|
|
743
1119
|
messageType: input.messageType,
|
|
744
1120
|
metadata: input.metadata
|
|
@@ -751,26 +1127,26 @@ async function forwardBridgeMessage(input) {
|
|
|
751
1127
|
input.logger.warn("linear_thenvoi_bridge.room_forward_recovering_with_fresh_room", {
|
|
752
1128
|
sessionId: input.sessionId,
|
|
753
1129
|
issueId: input.issueId,
|
|
754
|
-
previousRoomId: input.roomRecord.
|
|
1130
|
+
previousRoomId: input.roomRecord.bandRoomId,
|
|
755
1131
|
error: error instanceof Error ? error.message : String(error)
|
|
756
1132
|
});
|
|
757
1133
|
const recoveredRoom = await createFreshRoomRecord({
|
|
758
|
-
|
|
1134
|
+
bandRest: input.bandRest,
|
|
759
1135
|
store: input.store,
|
|
760
1136
|
sessionId: input.sessionId,
|
|
761
1137
|
issueId: input.issueId,
|
|
762
1138
|
logger: input.logger
|
|
763
1139
|
});
|
|
764
1140
|
await ensureRoomParticipants({
|
|
765
|
-
|
|
766
|
-
roomId: recoveredRoom.
|
|
1141
|
+
bandRest: input.bandRest,
|
|
1142
|
+
roomId: recoveredRoom.bandRoomId,
|
|
767
1143
|
handles: [input.hostAgentHandle],
|
|
768
1144
|
logger: input.logger
|
|
769
1145
|
});
|
|
770
1146
|
let attempt = 0;
|
|
771
1147
|
while (true) {
|
|
772
1148
|
try {
|
|
773
|
-
await input.
|
|
1149
|
+
await input.bandRest.createChatEvent(recoveredRoom.bandRoomId, {
|
|
774
1150
|
content: input.message,
|
|
775
1151
|
messageType: input.messageType,
|
|
776
1152
|
metadata: input.metadata
|
|
@@ -786,7 +1162,7 @@ async function forwardBridgeMessage(input) {
|
|
|
786
1162
|
input.logger.warn("linear_thenvoi_bridge.room_recreated_retrying", {
|
|
787
1163
|
sessionId: input.sessionId,
|
|
788
1164
|
issueId: input.issueId,
|
|
789
|
-
roomId: recoveredRoom.
|
|
1165
|
+
roomId: recoveredRoom.bandRoomId,
|
|
790
1166
|
attempt,
|
|
791
1167
|
delayMs,
|
|
792
1168
|
error: recoveredError instanceof Error ? recoveredError.message : String(recoveredError)
|
|
@@ -816,11 +1192,11 @@ function isRetryableRateLimitError(error) {
|
|
|
816
1192
|
}
|
|
817
1193
|
async function createFreshRoomRecord(input) {
|
|
818
1194
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
819
|
-
const created = await input.
|
|
1195
|
+
const created = await input.bandRest.createChat();
|
|
820
1196
|
const record = {
|
|
821
1197
|
linearSessionId: input.sessionId,
|
|
822
1198
|
linearIssueId: input.issueId,
|
|
823
|
-
|
|
1199
|
+
bandRoomId: created.id,
|
|
824
1200
|
status: "active",
|
|
825
1201
|
lastEventKey: null,
|
|
826
1202
|
createdAt: now,
|
|
@@ -846,7 +1222,7 @@ async function resolveRoomRecordImpl(input) {
|
|
|
846
1222
|
const linkedRecord = {
|
|
847
1223
|
linearSessionId: input.sessionId,
|
|
848
1224
|
linearIssueId: input.issueId,
|
|
849
|
-
|
|
1225
|
+
bandRoomId: existingByIssue.bandRoomId,
|
|
850
1226
|
status: "active",
|
|
851
1227
|
lastEventKey: existingBySession?.lastEventKey ?? null,
|
|
852
1228
|
createdAt: existingBySession?.createdAt ?? now2,
|
|
@@ -857,11 +1233,11 @@ async function resolveRoomRecordImpl(input) {
|
|
|
857
1233
|
}
|
|
858
1234
|
}
|
|
859
1235
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
860
|
-
const created = await input.
|
|
1236
|
+
const created = await input.bandRest.createChat();
|
|
861
1237
|
const createdRecord = {
|
|
862
1238
|
linearSessionId: input.sessionId,
|
|
863
1239
|
linearIssueId: input.issueId,
|
|
864
|
-
|
|
1240
|
+
bandRoomId: created.id,
|
|
865
1241
|
status: "active",
|
|
866
1242
|
lastEventKey: null,
|
|
867
1243
|
createdAt: now,
|
|
@@ -882,7 +1258,7 @@ async function ensureRoomParticipants(input) {
|
|
|
882
1258
|
}
|
|
883
1259
|
let existingParticipants;
|
|
884
1260
|
try {
|
|
885
|
-
existingParticipants = await input.
|
|
1261
|
+
existingParticipants = await input.bandRest.listChatParticipants(input.roomId);
|
|
886
1262
|
} catch (error) {
|
|
887
1263
|
if (isRetryableRateLimitError(error)) {
|
|
888
1264
|
input.logger.warn("linear_thenvoi_bridge.participant_list_rate_limited", {
|
|
@@ -902,7 +1278,7 @@ async function ensureRoomParticipants(input) {
|
|
|
902
1278
|
}
|
|
903
1279
|
let peersByHandle;
|
|
904
1280
|
try {
|
|
905
|
-
peersByHandle = await lookupPeerIdsByHandle(input.
|
|
1281
|
+
peersByHandle = await lookupPeerIdsByHandle(input.bandRest, input.roomId, missingHandles);
|
|
906
1282
|
} catch (error) {
|
|
907
1283
|
if (error instanceof UnsupportedFeatureError) {
|
|
908
1284
|
input.logger.warn("linear_thenvoi_bridge.peer_lookup_unavailable", {
|
|
@@ -924,7 +1300,7 @@ async function ensureRoomParticipants(input) {
|
|
|
924
1300
|
continue;
|
|
925
1301
|
}
|
|
926
1302
|
try {
|
|
927
|
-
await input.
|
|
1303
|
+
await input.bandRest.addChatParticipant(
|
|
928
1304
|
input.roomId,
|
|
929
1305
|
{
|
|
930
1306
|
participantId,
|
|
@@ -943,14 +1319,14 @@ async function ensureRoomParticipants(input) {
|
|
|
943
1319
|
}
|
|
944
1320
|
}
|
|
945
1321
|
}
|
|
946
|
-
async function lookupPeerIdsByHandle(
|
|
947
|
-
if (!
|
|
1322
|
+
async function lookupPeerIdsByHandle(bandRest, roomId, handles) {
|
|
1323
|
+
if (!bandRest.listPeers) {
|
|
948
1324
|
throw new UnsupportedFeatureError("Peer listing is not available in current REST adapter");
|
|
949
1325
|
}
|
|
950
1326
|
const targets = new Set(handles);
|
|
951
1327
|
const found = /* @__PURE__ */ new Map();
|
|
952
1328
|
for (let page = 1; page <= MAX_PEER_LOOKUP_PAGES && found.size < targets.size; page += 1) {
|
|
953
|
-
const response = await
|
|
1329
|
+
const response = await bandRest.listPeers({
|
|
954
1330
|
page,
|
|
955
1331
|
pageSize: PEER_PAGE_SIZE,
|
|
956
1332
|
notInChat: roomId
|
|
@@ -991,12 +1367,12 @@ async function selectRelevantPeerHandles(input) {
|
|
|
991
1367
|
});
|
|
992
1368
|
return configuredHandles;
|
|
993
1369
|
}
|
|
994
|
-
if (!input.
|
|
1370
|
+
if (!input.bandRest.listPeers) {
|
|
995
1371
|
return [];
|
|
996
1372
|
}
|
|
997
1373
|
let peers;
|
|
998
1374
|
try {
|
|
999
|
-
const response = await input.
|
|
1375
|
+
const response = await input.bandRest.listPeers({
|
|
1000
1376
|
page: 1,
|
|
1001
1377
|
pageSize: PEER_PAGE_SIZE,
|
|
1002
1378
|
notInChat: input.roomId
|
|
@@ -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,23 +1546,26 @@ 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(
|
|
1183
1563
|
record.linearSessionId,
|
|
1184
1564
|
record.linearIssueId,
|
|
1185
|
-
record.
|
|
1565
|
+
record.bandRoomId,
|
|
1186
1566
|
record.status,
|
|
1187
1567
|
record.lastEventKey ?? null,
|
|
1568
|
+
record.lastLinearActivityAt ?? null,
|
|
1188
1569
|
record.createdAt,
|
|
1189
1570
|
record.updatedAt
|
|
1190
1571
|
);
|
|
@@ -1228,7 +1609,7 @@ var SqliteSessionRoomStore = class {
|
|
|
1228
1609
|
).run(
|
|
1229
1610
|
request.eventKey,
|
|
1230
1611
|
request.linearSessionId,
|
|
1231
|
-
request.
|
|
1612
|
+
request.bandRoomId,
|
|
1232
1613
|
request.expectedContent,
|
|
1233
1614
|
request.messageType,
|
|
1234
1615
|
request.metadata ? JSON.stringify(request.metadata) : null,
|
|
@@ -1260,7 +1641,7 @@ var SqliteSessionRoomStore = class {
|
|
|
1260
1641
|
return rows.map((row) => ({
|
|
1261
1642
|
eventKey: row.event_key,
|
|
1262
1643
|
linearSessionId: row.linear_session_id,
|
|
1263
|
-
|
|
1644
|
+
bandRoomId: row.thenvoi_room_id,
|
|
1264
1645
|
expectedContent: row.expected_content,
|
|
1265
1646
|
messageType: row.message_type,
|
|
1266
1647
|
metadata: this.parseMetadata(row.metadata_json),
|
|
@@ -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,15 +1763,26 @@ 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) {
|
|
1365
1779
|
return {
|
|
1366
1780
|
linearSessionId: row.linear_session_id,
|
|
1367
1781
|
linearIssueId: row.linear_issue_id,
|
|
1368
|
-
|
|
1782
|
+
bandRoomId: 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
|
};
|
|
@@ -1420,20 +1835,22 @@ function parseSessionRoomRow(value) {
|
|
|
1420
1835
|
}
|
|
1421
1836
|
const linearSessionId = asString(row.linear_session_id);
|
|
1422
1837
|
const linearIssueId = asNullableString(row.linear_issue_id);
|
|
1423
|
-
const
|
|
1838
|
+
const bandRoomId = 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 || !
|
|
1844
|
+
if (!linearSessionId || linearIssueId === void 0 || !bandRoomId || !status || !isSessionStatus(status) || lastEventKey === void 0 || lastLinearActivityAt === void 0 || !createdAt || !updatedAt) {
|
|
1429
1845
|
return null;
|
|
1430
1846
|
}
|
|
1431
1847
|
return {
|
|
1432
1848
|
linear_session_id: linearSessionId,
|
|
1433
1849
|
linear_issue_id: linearIssueId,
|
|
1434
|
-
thenvoi_room_id:
|
|
1850
|
+
thenvoi_room_id: bandRoomId,
|
|
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
|
};
|
|
@@ -1451,20 +1868,20 @@ function parseBootstrapRequestRow(value) {
|
|
|
1451
1868
|
}
|
|
1452
1869
|
const eventKey = asString(row.event_key);
|
|
1453
1870
|
const linearSessionId = asString(row.linear_session_id);
|
|
1454
|
-
const
|
|
1871
|
+
const bandRoomId = asString(row.thenvoi_room_id);
|
|
1455
1872
|
const expectedContent = asString(row.expected_content);
|
|
1456
1873
|
const messageType = asString(row.message_type);
|
|
1457
1874
|
const metadataJson = asNullableString(row.metadata_json);
|
|
1458
1875
|
const createdAt = asString(row.created_at);
|
|
1459
1876
|
const expiresAt = asString(row.expires_at);
|
|
1460
1877
|
const processedAt = asNullableString(row.processed_at);
|
|
1461
|
-
if (!eventKey || !linearSessionId || !
|
|
1878
|
+
if (!eventKey || !linearSessionId || !bandRoomId || !expectedContent || !messageType || metadataJson === void 0 || !createdAt || !expiresAt || processedAt === void 0) {
|
|
1462
1879
|
return null;
|
|
1463
1880
|
}
|
|
1464
1881
|
return {
|
|
1465
1882
|
event_key: eventKey,
|
|
1466
1883
|
linear_session_id: linearSessionId,
|
|
1467
|
-
thenvoi_room_id:
|
|
1884
|
+
thenvoi_room_id: bandRoomId,
|
|
1468
1885
|
expected_content: expectedContent,
|
|
1469
1886
|
message_type: messageType,
|
|
1470
1887
|
metadata_json: metadataJson,
|
|
@@ -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 Band room ID to persist the session-room mapping. Pass this when creating a session from within a Band 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
|
+
bandRoomId: 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 Band 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.bandRest.createChatEvent(existing.bandRoomId, {
|
|
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: "band"
|
|
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.bandRoomId
|
|
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.bandRest.createChatEvent(existing.bandRoomId, {
|
|
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: "band"
|
|
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.bandRoomId
|
|
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
|
});
|