patchrelay 0.7.7 → 0.7.9
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/dist/build-info.json +3 -3
- package/dist/config.js +0 -3
- package/dist/index.js +0 -3
- package/dist/linear-client.js +10 -0
- package/dist/preflight.js +0 -6
- package/dist/service-webhook-processor.js +66 -13
- package/dist/service-webhooks.js +0 -29
- package/dist/stage-agent-activity-publisher.js +4 -4
- package/dist/stage-lifecycle-publisher.js +2 -5
- package/dist/webhook-agent-session-handler.js +6 -6
- package/dist/webhooks.js +132 -47
- package/package.json +1 -1
- package/dist/webhook-archive.js +0 -24
package/dist/build-info.json
CHANGED
package/dist/config.js
CHANGED
|
@@ -54,7 +54,6 @@ const configSchema = z.object({
|
|
|
54
54
|
level: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
55
55
|
format: z.literal("logfmt").default("logfmt"),
|
|
56
56
|
file_path: z.string().min(1).default(getDefaultLogPath()),
|
|
57
|
-
webhook_archive_dir: z.string().optional(),
|
|
58
57
|
}),
|
|
59
58
|
database: z.object({
|
|
60
59
|
path: z.string().min(1).default(getDefaultDatabasePath()),
|
|
@@ -314,7 +313,6 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
|
|
|
314
313
|
throw new Error(`Missing env var ${parsed.linear.token_encryption_key_env}`);
|
|
315
314
|
}
|
|
316
315
|
const logFilePath = env.PATCHRELAY_LOG_FILE ?? parsed.logging.file_path;
|
|
317
|
-
const webhookArchiveDir = env.PATCHRELAY_WEBHOOK_ARCHIVE_DIR ?? parsed.logging.webhook_archive_dir;
|
|
318
316
|
const oauthRedirectUri = parsed.linear.oauth.redirect_uri ?? deriveLinearOAuthRedirectUri(parsed.server);
|
|
319
317
|
const config = {
|
|
320
318
|
server: {
|
|
@@ -333,7 +331,6 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
|
|
|
333
331
|
level: env.PATCHRELAY_LOG_LEVEL ?? parsed.logging.level,
|
|
334
332
|
format: parsed.logging.format,
|
|
335
333
|
filePath: ensureAbsolutePath(logFilePath),
|
|
336
|
-
...(webhookArchiveDir ? { webhookArchiveDir: ensureAbsolutePath(webhookArchiveDir) } : {}),
|
|
337
334
|
},
|
|
338
335
|
database: {
|
|
339
336
|
path: ensureAbsolutePath(env.PATCHRELAY_DB_PATH ?? parsed.database.path),
|
package/dist/index.js
CHANGED
|
@@ -24,9 +24,6 @@ async function main() {
|
|
|
24
24
|
await enforceServiceEnvPermissions(getAdjacentEnvFilePaths(configPath).serviceEnvPath);
|
|
25
25
|
await ensureDir(dirname(config.database.path));
|
|
26
26
|
await ensureDir(dirname(config.logging.filePath));
|
|
27
|
-
if (config.logging.webhookArchiveDir) {
|
|
28
|
-
await ensureDir(config.logging.webhookArchiveDir);
|
|
29
|
-
}
|
|
30
27
|
for (const project of config.projects) {
|
|
31
28
|
await ensureDir(project.worktreeRoot);
|
|
32
29
|
}
|
package/dist/linear-client.js
CHANGED
|
@@ -15,6 +15,10 @@ export class LinearGraphqlClient {
|
|
|
15
15
|
identifier
|
|
16
16
|
title
|
|
17
17
|
url
|
|
18
|
+
delegate {
|
|
19
|
+
id
|
|
20
|
+
name
|
|
21
|
+
}
|
|
18
22
|
state {
|
|
19
23
|
id
|
|
20
24
|
name
|
|
@@ -65,6 +69,10 @@ export class LinearGraphqlClient {
|
|
|
65
69
|
identifier
|
|
66
70
|
title
|
|
67
71
|
url
|
|
72
|
+
delegate {
|
|
73
|
+
id
|
|
74
|
+
name
|
|
75
|
+
}
|
|
68
76
|
state {
|
|
69
77
|
id
|
|
70
78
|
name
|
|
@@ -290,6 +298,8 @@ export class LinearGraphqlClient {
|
|
|
290
298
|
...(issue.state?.name ? { stateName: issue.state.name } : {}),
|
|
291
299
|
...(issue.team?.id ? { teamId: issue.team.id } : {}),
|
|
292
300
|
...(issue.team?.key ? { teamKey: issue.team.key } : {}),
|
|
301
|
+
...(issue.delegate?.id ? { delegateId: issue.delegate.id } : {}),
|
|
302
|
+
...(issue.delegate?.name ? { delegateName: issue.delegate.name } : {}),
|
|
293
303
|
workflowStates: (issue.team?.states?.nodes ?? []).map((state) => ({
|
|
294
304
|
id: state.id,
|
|
295
305
|
name: state.name,
|
package/dist/preflight.js
CHANGED
|
@@ -66,12 +66,6 @@ export async function runPreflight(config) {
|
|
|
66
66
|
checks.push(...checkPath("database", path.dirname(config.database.path), "directory", { createIfMissing: true, writable: true }));
|
|
67
67
|
checks.push(checkDatabaseSchema(config));
|
|
68
68
|
checks.push(...checkPath("logging", path.dirname(config.logging.filePath), "directory", { createIfMissing: true, writable: true }));
|
|
69
|
-
if (config.logging.webhookArchiveDir) {
|
|
70
|
-
checks.push(...checkPath("archive", config.logging.webhookArchiveDir, "directory", { createIfMissing: true, writable: true }));
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
checks.push(warn("archive", "Raw webhook archival is disabled"));
|
|
74
|
-
}
|
|
75
69
|
if (config.projects.length === 0) {
|
|
76
70
|
checks.push(warn("projects", "No projects are configured yet; add one with `patchrelay project apply <id> <repo-path>` before connecting Linear"));
|
|
77
71
|
}
|
|
@@ -10,6 +10,7 @@ import { normalizeWebhook } from "./webhooks.js";
|
|
|
10
10
|
export class ServiceWebhookProcessor {
|
|
11
11
|
config;
|
|
12
12
|
stores;
|
|
13
|
+
linearProvider;
|
|
13
14
|
enqueueIssue;
|
|
14
15
|
logger;
|
|
15
16
|
feed;
|
|
@@ -20,6 +21,7 @@ export class ServiceWebhookProcessor {
|
|
|
20
21
|
constructor(config, stores, linearProvider, codex, enqueueIssue, logger, feed) {
|
|
21
22
|
this.config = config;
|
|
22
23
|
this.stores = stores;
|
|
24
|
+
this.linearProvider = linearProvider;
|
|
23
25
|
this.enqueueIssue = enqueueIssue;
|
|
24
26
|
this.logger = logger;
|
|
25
27
|
this.feed = feed;
|
|
@@ -115,13 +117,15 @@ export class ServiceWebhookProcessor {
|
|
|
115
117
|
}
|
|
116
118
|
this.stores.webhookEvents.assignWebhookProject(webhookEventId, project.id);
|
|
117
119
|
const receipt = this.ensureEventReceipt(event, project.id, normalized.issue.id);
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
+
const hydrated = await this.hydrateIssueContext(project.id, normalized);
|
|
121
|
+
const hydratedIssue = hydrated.issue ?? normalized.issue;
|
|
122
|
+
const issueState = this.desiredStageRecorder.record(project, hydrated, receipt ? { eventReceiptId: receipt.id } : undefined);
|
|
123
|
+
const observation = describeWebhookObservation(hydrated, issueState.delegatedToPatchRelay);
|
|
120
124
|
if (observation) {
|
|
121
125
|
this.feed?.publish({
|
|
122
126
|
level: "info",
|
|
123
127
|
kind: observation.kind,
|
|
124
|
-
issueKey:
|
|
128
|
+
issueKey: hydratedIssue.identifier,
|
|
125
129
|
projectId: project.id,
|
|
126
130
|
...(observation.status ? { status: observation.status } : {}),
|
|
127
131
|
summary: observation.summary,
|
|
@@ -129,45 +133,45 @@ export class ServiceWebhookProcessor {
|
|
|
129
133
|
});
|
|
130
134
|
}
|
|
131
135
|
await this.agentSessionHandler.handle({
|
|
132
|
-
normalized,
|
|
136
|
+
normalized: hydrated,
|
|
133
137
|
project,
|
|
134
138
|
issue: issueState.issue,
|
|
135
139
|
desiredStage: issueState.desiredStage,
|
|
136
140
|
delegatedToPatchRelay: issueState.delegatedToPatchRelay,
|
|
137
141
|
});
|
|
138
|
-
await this.commentHandler.handle(
|
|
142
|
+
await this.commentHandler.handle(hydrated, project);
|
|
139
143
|
this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "processed");
|
|
140
144
|
this.markEventReceiptProcessed(event.webhookId, "processed");
|
|
141
145
|
if (issueState.desiredStage) {
|
|
142
146
|
this.feed?.publish({
|
|
143
147
|
level: "info",
|
|
144
148
|
kind: "stage",
|
|
145
|
-
issueKey:
|
|
149
|
+
issueKey: hydratedIssue.identifier,
|
|
146
150
|
projectId: project.id,
|
|
147
151
|
stage: issueState.desiredStage,
|
|
148
152
|
status: "queued",
|
|
149
153
|
summary: `Queued ${issueState.desiredStage} workflow`,
|
|
150
|
-
detail: `Triggered by ${
|
|
154
|
+
detail: `Triggered by ${hydrated.triggerEvent}${hydratedIssue.stateName ? ` from ${hydratedIssue.stateName}` : ""}.`,
|
|
151
155
|
});
|
|
152
156
|
this.logger.info({
|
|
153
157
|
webhookEventId,
|
|
154
158
|
webhookId: event.webhookId,
|
|
155
159
|
projectId: project.id,
|
|
156
|
-
issueKey:
|
|
157
|
-
issueId:
|
|
160
|
+
issueKey: hydratedIssue.identifier,
|
|
161
|
+
issueId: hydratedIssue.id,
|
|
158
162
|
desiredStage: issueState.desiredStage,
|
|
159
163
|
delegatedToPatchRelay: issueState.delegatedToPatchRelay,
|
|
160
164
|
}, "Recorded desired stage from webhook and enqueued issue execution");
|
|
161
|
-
this.enqueueIssue(project.id,
|
|
165
|
+
this.enqueueIssue(project.id, hydratedIssue.id);
|
|
162
166
|
return;
|
|
163
167
|
}
|
|
164
168
|
this.logger.info({
|
|
165
169
|
webhookEventId,
|
|
166
170
|
webhookId: event.webhookId,
|
|
167
171
|
projectId: project.id,
|
|
168
|
-
issueKey:
|
|
169
|
-
issueId:
|
|
170
|
-
triggerEvent:
|
|
172
|
+
issueKey: hydratedIssue.identifier,
|
|
173
|
+
issueId: hydratedIssue.id,
|
|
174
|
+
triggerEvent: hydrated.triggerEvent,
|
|
171
175
|
delegatedToPatchRelay: issueState.delegatedToPatchRelay,
|
|
172
176
|
}, "Processed webhook without enqueuing a new stage run");
|
|
173
177
|
}
|
|
@@ -194,6 +198,37 @@ export class ServiceWebhookProcessor {
|
|
|
194
198
|
throw err;
|
|
195
199
|
}
|
|
196
200
|
}
|
|
201
|
+
async hydrateIssueContext(projectId, normalized) {
|
|
202
|
+
if (!normalized.issue) {
|
|
203
|
+
return normalized;
|
|
204
|
+
}
|
|
205
|
+
if (normalized.triggerEvent !== "agentSessionCreated" && normalized.triggerEvent !== "agentPrompted") {
|
|
206
|
+
return normalized;
|
|
207
|
+
}
|
|
208
|
+
if (hasCompleteIssueContext(normalized.issue)) {
|
|
209
|
+
return normalized;
|
|
210
|
+
}
|
|
211
|
+
const linear = await this.linearProvider.forProject(projectId);
|
|
212
|
+
if (!linear) {
|
|
213
|
+
return normalized;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const liveIssue = await linear.getIssue(normalized.issue.id);
|
|
217
|
+
return {
|
|
218
|
+
...normalized,
|
|
219
|
+
issue: mergeIssueMetadata(normalized.issue, liveIssue),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
this.logger.warn({
|
|
224
|
+
projectId,
|
|
225
|
+
issueId: normalized.issue.id,
|
|
226
|
+
triggerEvent: normalized.triggerEvent,
|
|
227
|
+
error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
|
|
228
|
+
}, "Failed to hydrate sparse Linear issue context for agent session webhook");
|
|
229
|
+
return normalized;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
197
232
|
assignEventReceiptContext(webhookId, projectId, linearIssueId) {
|
|
198
233
|
const receipt = this.lookupEventReceipt(webhookId);
|
|
199
234
|
if (!receipt) {
|
|
@@ -234,6 +269,24 @@ export class ServiceWebhookProcessor {
|
|
|
234
269
|
return this.stores.eventReceipts.getEventReceipt(inserted.id);
|
|
235
270
|
}
|
|
236
271
|
}
|
|
272
|
+
function hasCompleteIssueContext(issue) {
|
|
273
|
+
return Boolean(issue.stateName && issue.delegateId && issue.teamId && issue.teamKey);
|
|
274
|
+
}
|
|
275
|
+
function mergeIssueMetadata(issue, liveIssue) {
|
|
276
|
+
return {
|
|
277
|
+
...issue,
|
|
278
|
+
...(issue.identifier ? {} : liveIssue.identifier ? { identifier: liveIssue.identifier } : {}),
|
|
279
|
+
...(issue.title ? {} : liveIssue.title ? { title: liveIssue.title } : {}),
|
|
280
|
+
...(issue.url ? {} : liveIssue.url ? { url: liveIssue.url } : {}),
|
|
281
|
+
...(issue.teamId ? {} : liveIssue.teamId ? { teamId: liveIssue.teamId } : {}),
|
|
282
|
+
...(issue.teamKey ? {} : liveIssue.teamKey ? { teamKey: liveIssue.teamKey } : {}),
|
|
283
|
+
...(issue.stateId ? {} : liveIssue.stateId ? { stateId: liveIssue.stateId } : {}),
|
|
284
|
+
...(issue.stateName ? {} : liveIssue.stateName ? { stateName: liveIssue.stateName } : {}),
|
|
285
|
+
...(issue.delegateId ? {} : liveIssue.delegateId ? { delegateId: liveIssue.delegateId } : {}),
|
|
286
|
+
...(issue.delegateName ? {} : liveIssue.delegateName ? { delegateName: liveIssue.delegateName } : {}),
|
|
287
|
+
labelNames: issue.labelNames.length > 0 ? issue.labelNames : (liveIssue.labels ?? []).map((label) => label.name),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
237
290
|
function describeWebhookObservation(normalized, delegatedToPatchRelay) {
|
|
238
291
|
switch (normalized.triggerEvent) {
|
|
239
292
|
case "delegateChanged":
|
package/dist/service-webhooks.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { archiveWebhook } from "./webhook-archive.js";
|
|
2
1
|
import { normalizeWebhook } from "./webhooks.js";
|
|
3
2
|
import { redactSensitiveHeaders, timestampMsWithinSkew, verifyHmacSha256Hex } from "./utils.js";
|
|
4
3
|
export async function acceptIncomingWebhook(params) {
|
|
@@ -35,15 +34,6 @@ export async function acceptIncomingWebhook(params) {
|
|
|
35
34
|
const headersJson = JSON.stringify(sanitizedHeaders);
|
|
36
35
|
const payloadJson = JSON.stringify(payload);
|
|
37
36
|
logWebhookSummary(params.logger, normalized);
|
|
38
|
-
await archiveAcceptedPayload({
|
|
39
|
-
config: params.config,
|
|
40
|
-
logger: params.logger,
|
|
41
|
-
webhookId: params.webhookId,
|
|
42
|
-
receivedAt,
|
|
43
|
-
headers: params.headers,
|
|
44
|
-
rawBody: params.rawBody,
|
|
45
|
-
payload,
|
|
46
|
-
});
|
|
47
37
|
const stored = params.stores.webhookEvents.insertWebhookEvent({
|
|
48
38
|
webhookId: params.webhookId,
|
|
49
39
|
receivedAt,
|
|
@@ -127,22 +117,3 @@ function logWebhookSummary(logger, normalized) {
|
|
|
127
117
|
issueId: normalized.issue?.id,
|
|
128
118
|
}, "Webhook metadata");
|
|
129
119
|
}
|
|
130
|
-
async function archiveAcceptedPayload(params) {
|
|
131
|
-
if (!params.config.logging.webhookArchiveDir) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
const archivePath = await archiveWebhook({
|
|
136
|
-
archiveDir: params.config.logging.webhookArchiveDir,
|
|
137
|
-
webhookId: params.webhookId,
|
|
138
|
-
receivedAt: params.receivedAt,
|
|
139
|
-
headers: redactSensitiveHeaders(params.headers),
|
|
140
|
-
rawBody: params.rawBody,
|
|
141
|
-
payload: params.payload,
|
|
142
|
-
});
|
|
143
|
-
params.logger.debug({ webhookId: params.webhookId, archivePath }, "Archived webhook to local file");
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
params.logger.error({ webhookId: params.webhookId, error }, "Failed to archive webhook to local file");
|
|
147
|
-
}
|
|
148
|
-
}
|
|
@@ -8,7 +8,7 @@ export class StageAgentActivityPublisher {
|
|
|
8
8
|
this.linearProvider = linearProvider;
|
|
9
9
|
this.logger = logger;
|
|
10
10
|
}
|
|
11
|
-
async publishForSession(projectId, agentSessionId, content) {
|
|
11
|
+
async publishForSession(projectId, agentSessionId, content, options) {
|
|
12
12
|
const linear = await this.linearProvider.forProject(projectId);
|
|
13
13
|
if (!linear) {
|
|
14
14
|
return;
|
|
@@ -17,7 +17,7 @@ export class StageAgentActivityPublisher {
|
|
|
17
17
|
await linear.createAgentActivity({
|
|
18
18
|
agentSessionId,
|
|
19
19
|
content,
|
|
20
|
-
ephemeral: content.type === "thought" || content.type === "action",
|
|
20
|
+
ephemeral: options?.ephemeral ?? (content.type === "thought" || content.type === "action"),
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
catch (error) {
|
|
@@ -27,11 +27,11 @@ export class StageAgentActivityPublisher {
|
|
|
27
27
|
}, "Failed to publish Linear agent activity");
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
async publishForIssue(issue, content) {
|
|
30
|
+
async publishForIssue(issue, content, options) {
|
|
31
31
|
if (!issue.activeAgentSessionId) {
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
-
await this.publishForSession(issue.projectId, issue.activeAgentSessionId, content);
|
|
34
|
+
await this.publishForSession(issue.projectId, issue.activeAgentSessionId, content, options);
|
|
35
35
|
}
|
|
36
36
|
async updateSession(params) {
|
|
37
37
|
const linear = await this.linearProvider.forProject(params.projectId);
|
|
@@ -84,12 +84,9 @@ export class StageLifecyclePublisher {
|
|
|
84
84
|
await linear.createAgentActivity({
|
|
85
85
|
agentSessionId: issue.activeAgentSessionId,
|
|
86
86
|
content: {
|
|
87
|
-
type: "
|
|
88
|
-
|
|
89
|
-
parameter: stage,
|
|
90
|
-
result: `PatchRelay started the ${stage} workflow.`,
|
|
87
|
+
type: "thought",
|
|
88
|
+
body: `PatchRelay started the ${stage} workflow and is working in the background.`,
|
|
91
89
|
},
|
|
92
|
-
ephemeral: true,
|
|
93
90
|
});
|
|
94
91
|
return true;
|
|
95
92
|
}
|
|
@@ -66,9 +66,9 @@ export class AgentSessionWebhookHandler {
|
|
|
66
66
|
if (desiredStage) {
|
|
67
67
|
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildPreparingSessionPlan(desiredStage)));
|
|
68
68
|
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
69
|
-
type: "
|
|
70
|
-
body: `PatchRelay
|
|
71
|
-
});
|
|
69
|
+
type: "thought",
|
|
70
|
+
body: `PatchRelay started working on the ${desiredStage} workflow and is preparing the workspace.`,
|
|
71
|
+
}, { ephemeral: false });
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
if (activeStage) {
|
|
@@ -137,9 +137,9 @@ export class AgentSessionWebhookHandler {
|
|
|
137
137
|
if (!activeRunLease && desiredStage) {
|
|
138
138
|
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildPreparingSessionPlan(desiredStage)));
|
|
139
139
|
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
140
|
-
type: "
|
|
141
|
-
body: `PatchRelay
|
|
142
|
-
});
|
|
140
|
+
type: "thought",
|
|
141
|
+
body: `PatchRelay is preparing the ${desiredStage} workflow from your latest prompt.`,
|
|
142
|
+
}, { ephemeral: false });
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
145
|
if (!activeRunLease && !desiredStage && (promptBody || promptContext)) {
|
package/dist/webhooks.js
CHANGED
|
@@ -1,9 +1,55 @@
|
|
|
1
|
+
function getPayloadRecord(payload) {
|
|
2
|
+
return payload;
|
|
3
|
+
}
|
|
4
|
+
function getPayloadData(payload) {
|
|
5
|
+
return asRecord(getPayloadRecord(payload).data) ?? getPayloadRecord(payload);
|
|
6
|
+
}
|
|
7
|
+
function getNestedRecord(record, path) {
|
|
8
|
+
let current = record;
|
|
9
|
+
for (const segment of path) {
|
|
10
|
+
const currentRecord = asRecord(current);
|
|
11
|
+
if (!currentRecord) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
current = currentRecord[segment];
|
|
15
|
+
}
|
|
16
|
+
return asRecord(current);
|
|
17
|
+
}
|
|
18
|
+
function getFirstNestedRecord(record, paths) {
|
|
19
|
+
for (const path of paths) {
|
|
20
|
+
const candidate = getNestedRecord(record, path);
|
|
21
|
+
if (candidate) {
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function looksLikeIssueRecord(record) {
|
|
28
|
+
if (!record) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return Boolean(getString(record, "identifier") ||
|
|
32
|
+
getString(record, "title") ||
|
|
33
|
+
getString(record, "delegateId") ||
|
|
34
|
+
asRecord(record.delegate) ||
|
|
35
|
+
asRecord(record.team) ||
|
|
36
|
+
asRecord(record.state) ||
|
|
37
|
+
Array.isArray(record.labels));
|
|
38
|
+
}
|
|
1
39
|
function deriveTriggerEvent(payload) {
|
|
2
|
-
|
|
3
|
-
|
|
40
|
+
const data = getPayloadData(payload);
|
|
41
|
+
const hasAgentSession = Boolean(getFirstNestedRecord(data, [
|
|
42
|
+
["agentSession"],
|
|
43
|
+
["session"],
|
|
44
|
+
["agentSessionEvent", "agentSession"],
|
|
45
|
+
["payload", "agentSession"],
|
|
46
|
+
["resource", "agentSession"],
|
|
47
|
+
])) || Boolean(getString(data, "agentSessionId"));
|
|
48
|
+
if (payload.type === "AgentSessionEvent" || payload.type === "AgentSession" || hasAgentSession) {
|
|
49
|
+
if (payload.action === "created" || payload.action === "create") {
|
|
4
50
|
return "agentSessionCreated";
|
|
5
51
|
}
|
|
6
|
-
if (payload.action === "prompted") {
|
|
52
|
+
if (payload.action === "prompted" || payload.action === "prompt") {
|
|
7
53
|
return "agentPrompted";
|
|
8
54
|
}
|
|
9
55
|
return "issueUpdated";
|
|
@@ -93,28 +139,42 @@ function extractLabelNames(record) {
|
|
|
93
139
|
return [];
|
|
94
140
|
}
|
|
95
141
|
function extractIssueMetadata(payload) {
|
|
96
|
-
const data =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
142
|
+
const data = getPayloadData(payload);
|
|
143
|
+
const sessionRecord = getFirstNestedRecord(data, [
|
|
144
|
+
["agentSession"],
|
|
145
|
+
["session"],
|
|
146
|
+
["agentSessionEvent", "agentSession"],
|
|
147
|
+
["payload", "agentSession"],
|
|
148
|
+
["resource", "agentSession"],
|
|
149
|
+
]) ?? data;
|
|
150
|
+
const commentRecord = getFirstNestedRecord(data, [["comment"]]);
|
|
151
|
+
const notificationRecord = getFirstNestedRecord(data, [["notification"]]) ?? data;
|
|
100
152
|
const issueRecord = payload.type === "Issue"
|
|
101
153
|
? data
|
|
102
|
-
: payload.type === "
|
|
103
|
-
? (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
154
|
+
: payload.type === "AppUserNotification"
|
|
155
|
+
? getFirstNestedRecord(notificationRecord, [["issue"], ["comment", "issue"]]) ?? getFirstNestedRecord(data, [["issue"]])
|
|
156
|
+
: getFirstNestedRecord(data, [
|
|
157
|
+
["issue"],
|
|
158
|
+
["agentSession", "issue"],
|
|
159
|
+
["session", "issue"],
|
|
160
|
+
["agentSessionEvent", "issue"],
|
|
161
|
+
["agentSessionEvent", "agentSession", "issue"],
|
|
162
|
+
["payload", "issue"],
|
|
163
|
+
["payload", "agentSession", "issue"],
|
|
164
|
+
["resource", "issue"],
|
|
165
|
+
["resource", "agentSession", "issue"],
|
|
166
|
+
["comment", "issue"],
|
|
167
|
+
["comment", "parent", "issue"],
|
|
168
|
+
["comment", "commentThread", "issue"],
|
|
169
|
+
["comment", "parentEntity", "issue"],
|
|
170
|
+
["parent", "issue"],
|
|
171
|
+
["commentThread", "issue"],
|
|
172
|
+
["parentEntity", "issue"],
|
|
173
|
+
["notification", "issue"],
|
|
174
|
+
["notification", "comment", "issue"],
|
|
175
|
+
]) ??
|
|
176
|
+
(looksLikeIssueRecord(sessionRecord) ? sessionRecord : undefined) ??
|
|
177
|
+
(looksLikeIssueRecord(commentRecord) ? commentRecord : undefined);
|
|
118
178
|
if (!issueRecord) {
|
|
119
179
|
return undefined;
|
|
120
180
|
}
|
|
@@ -184,16 +244,24 @@ function extractActorMetadata(payload) {
|
|
|
184
244
|
return fallbacks.find(Boolean);
|
|
185
245
|
}
|
|
186
246
|
function extractCommentMetadata(payload) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
247
|
+
const data = getPayloadData(payload);
|
|
248
|
+
const commentRecord = payload.type === "Comment"
|
|
249
|
+
? data
|
|
250
|
+
: getFirstNestedRecord(data, [
|
|
251
|
+
["comment"],
|
|
252
|
+
["agentSession", "comment"],
|
|
253
|
+
["session", "comment"],
|
|
254
|
+
["agentSessionEvent", "comment"],
|
|
255
|
+
["payload", "comment"],
|
|
256
|
+
["resource", "comment"],
|
|
257
|
+
["notification", "comment"],
|
|
258
|
+
]);
|
|
259
|
+
if (!commentRecord) {
|
|
192
260
|
return undefined;
|
|
193
261
|
}
|
|
194
|
-
const id = getString(
|
|
195
|
-
const body = getString(
|
|
196
|
-
const userRecord = asRecord(
|
|
262
|
+
const id = getString(commentRecord, "id");
|
|
263
|
+
const body = getString(commentRecord, "body");
|
|
264
|
+
const userRecord = asRecord(commentRecord.user);
|
|
197
265
|
const userName = getString(userRecord ?? {}, "name");
|
|
198
266
|
if (!id) {
|
|
199
267
|
return undefined;
|
|
@@ -205,23 +273,43 @@ function extractCommentMetadata(payload) {
|
|
|
205
273
|
};
|
|
206
274
|
}
|
|
207
275
|
function extractAgentSessionMetadata(payload) {
|
|
208
|
-
|
|
276
|
+
const data = getPayloadData(payload);
|
|
277
|
+
const sessionRecord = getFirstNestedRecord(data, [
|
|
278
|
+
["agentSession"],
|
|
279
|
+
["session"],
|
|
280
|
+
["agentSessionEvent", "agentSession"],
|
|
281
|
+
["payload", "agentSession"],
|
|
282
|
+
["resource", "agentSession"],
|
|
283
|
+
]) ?? (payload.type === "AgentSession" ? data : undefined);
|
|
284
|
+
if (payload.type !== "AgentSessionEvent" && payload.type !== "AgentSession" && !sessionRecord && !getString(data, "agentSessionId")) {
|
|
209
285
|
return undefined;
|
|
210
286
|
}
|
|
211
|
-
const
|
|
212
|
-
if (!data) {
|
|
213
|
-
return undefined;
|
|
214
|
-
}
|
|
215
|
-
const sessionRecord = asRecord(data.agentSession) ?? data;
|
|
216
|
-
const id = getString(sessionRecord, "id") ?? getString(data, "agentSessionId");
|
|
287
|
+
const id = getString(sessionRecord ?? {}, "id") ?? getString(data, "agentSessionId");
|
|
217
288
|
if (!id) {
|
|
218
289
|
return undefined;
|
|
219
290
|
}
|
|
220
|
-
const agentActivity =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
291
|
+
const agentActivity = getFirstNestedRecord(data, [
|
|
292
|
+
["agentActivity"],
|
|
293
|
+
["agentSession", "agentActivity"],
|
|
294
|
+
["session", "agentActivity"],
|
|
295
|
+
["agentSessionEvent", "agentActivity"],
|
|
296
|
+
["payload", "agentActivity"],
|
|
297
|
+
["resource", "agentActivity"],
|
|
298
|
+
]);
|
|
299
|
+
const commentRecord = getFirstNestedRecord(data, [
|
|
300
|
+
["comment"],
|
|
301
|
+
["agentSession", "comment"],
|
|
302
|
+
["session", "comment"],
|
|
303
|
+
["agentSessionEvent", "comment"],
|
|
304
|
+
["payload", "comment"],
|
|
305
|
+
["resource", "comment"],
|
|
306
|
+
]) ??
|
|
307
|
+
getFirstNestedRecord(sessionRecord, [["comment"]]);
|
|
308
|
+
const promptContext = getString(data, "promptContext") ?? getString(sessionRecord ?? {}, "promptContext");
|
|
309
|
+
const promptBody = getString(agentActivity ?? {}, "body") ??
|
|
310
|
+
getString(commentRecord ?? {}, "body") ??
|
|
311
|
+
getString(data, "body");
|
|
312
|
+
const issueCommentId = getString(commentRecord ?? {}, "id") ?? getString(data, "issueCommentId");
|
|
225
313
|
return {
|
|
226
314
|
id,
|
|
227
315
|
...(promptContext ? { promptContext } : {}),
|
|
@@ -230,10 +318,7 @@ function extractAgentSessionMetadata(payload) {
|
|
|
230
318
|
};
|
|
231
319
|
}
|
|
232
320
|
function extractInstallationMetadata(payload) {
|
|
233
|
-
const data =
|
|
234
|
-
if (!data) {
|
|
235
|
-
return undefined;
|
|
236
|
-
}
|
|
321
|
+
const data = getPayloadData(payload);
|
|
237
322
|
if (payload.type === "PermissionChange") {
|
|
238
323
|
const organizationId = getString(data, "organizationId");
|
|
239
324
|
const oauthClientId = getString(data, "oauthClientId");
|
package/package.json
CHANGED
package/dist/webhook-archive.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
const REDACTED_HEADERS = new Set(["authorization", "cookie", "set-cookie", "linear-signature"]);
|
|
4
|
-
function sanitizePathSegment(value) {
|
|
5
|
-
return value.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
6
|
-
}
|
|
7
|
-
export async function archiveWebhook(params) {
|
|
8
|
-
const datePrefix = params.receivedAt.slice(0, 10);
|
|
9
|
-
const directory = path.join(params.archiveDir, datePrefix);
|
|
10
|
-
const fileName = `${params.receivedAt.replace(/[:.]/g, "-")}-${sanitizePathSegment(params.webhookId)}.json`;
|
|
11
|
-
const filePath = path.join(directory, fileName);
|
|
12
|
-
await mkdir(directory, { recursive: true });
|
|
13
|
-
await writeFile(filePath, JSON.stringify({
|
|
14
|
-
webhookId: params.webhookId,
|
|
15
|
-
receivedAt: params.receivedAt,
|
|
16
|
-
headers: redactHeaders(params.headers),
|
|
17
|
-
rawBodyUtf8: params.rawBody.toString("utf8"),
|
|
18
|
-
payload: params.payload,
|
|
19
|
-
}, null, 2), "utf8");
|
|
20
|
-
return filePath;
|
|
21
|
-
}
|
|
22
|
-
function redactHeaders(headers) {
|
|
23
|
-
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, REDACTED_HEADERS.has(key.toLowerCase()) ? "[redacted]" : value]));
|
|
24
|
-
}
|