@getpaseo/server 0.1.93 → 0.1.95
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/server/server/agent/agent-manager.d.ts +14 -0
- package/dist/server/server/agent/agent-manager.js +36 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts +1 -0
- package/dist/server/server/agent/providers/claude/agent.d.ts +1 -0
- package/dist/server/server/agent/providers/claude/agent.js +58 -3
- package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +2 -2
- package/dist/server/server/agent/providers/mock-load-test-agent.js +55 -28
- package/dist/server/server/agent/providers/pi/agent.js +3 -1
- package/dist/server/server/agent/providers/pi/session-descriptor.d.ts +5 -0
- package/dist/server/server/agent/providers/pi/session-descriptor.js +74 -12
- package/dist/server/server/agent/runtime-mcp-config.d.ts +6 -0
- package/dist/server/server/agent/runtime-mcp-config.js +3 -0
- package/dist/server/server/auth.d.ts +13 -0
- package/dist/server/server/auth.js +35 -0
- package/dist/server/server/bootstrap.d.ts +2 -0
- package/dist/server/server/bootstrap.js +28 -1
- package/dist/server/server/config.js +3 -1
- package/dist/server/server/daemon-config-store.js +3 -0
- package/dist/server/server/loop-service.d.ts +6 -6
- package/dist/server/server/persisted-config.d.ts +61 -0
- package/dist/server/server/persisted-config.js +2 -0
- package/dist/server/server/session.d.ts +6 -0
- package/dist/server/server/session.js +115 -7
- package/dist/server/server/websocket-server.js +2 -0
- package/dist/server/server/workspace-git-service.d.ts +4 -1
- package/dist/server/server/workspace-git-service.js +40 -22
- package/dist/server/server/workspace-reconciliation-service.js +10 -5
- package/dist/server/services/github-service.d.ts +57 -0
- package/dist/server/services/github-service.js +434 -5
- package/dist/server/terminal/terminal-session-controller.d.ts +6 -0
- package/dist/server/terminal/terminal-session-controller.js +36 -2
- package/dist/src/server/persisted-config.js +2 -0
- package/package.json +5 -5
|
@@ -4,6 +4,12 @@ import { resolveGitHubRemote } from "../utils/github-remote.js";
|
|
|
4
4
|
import { runGitCommand } from "../utils/run-git-command.js";
|
|
5
5
|
import { execCommand } from "../utils/spawn.js";
|
|
6
6
|
const DEFAULT_GITHUB_CACHE_TTL_MS = 30000;
|
|
7
|
+
const CHECK_ANNOTATION_PAGE_MAX = 20;
|
|
8
|
+
const CHECK_LOG_TAIL_MAX_LINES = 200;
|
|
9
|
+
const CHECK_LOG_TAIL_MAX_BYTES = 16 * 1024;
|
|
10
|
+
const CHECK_LOG_TAIL_CACHE_MAX_ENTRIES = 128;
|
|
11
|
+
const ACTIONS_JOB_PAGE_MAX = 100;
|
|
12
|
+
const FAILED_CHECK_JOB_LIMIT = 5;
|
|
7
13
|
export const GITHUB_POLL_FAST_INTERVAL_MS = 20000;
|
|
8
14
|
export const GITHUB_POLL_SLOW_INTERVAL_MS = 120000;
|
|
9
15
|
export const GITHUB_POLL_ERROR_BACKOFF_CAP_MS = 300000;
|
|
@@ -35,6 +41,7 @@ const GitHubPullRequestSummarySchema = z.object({
|
|
|
35
41
|
});
|
|
36
42
|
const PullRequestCheckRunNodeSchema = z.object({
|
|
37
43
|
__typename: z.literal("CheckRun"),
|
|
44
|
+
databaseId: z.number().nullable().optional(),
|
|
38
45
|
name: z.string(),
|
|
39
46
|
workflowName: z.string().nullable().optional(),
|
|
40
47
|
conclusion: z.string().nullable().optional(),
|
|
@@ -69,6 +76,54 @@ const PullRequestStatusCheckRollupArraySchema = z.array(z.unknown());
|
|
|
69
76
|
const LegacyPullRequestStatusCheckRollupSchema = z.object({
|
|
70
77
|
contexts: z.array(z.unknown()),
|
|
71
78
|
});
|
|
79
|
+
const GitHubCheckRunDetailsSchema = z.object({
|
|
80
|
+
id: z.number(),
|
|
81
|
+
name: z.string().catch(""),
|
|
82
|
+
status: z.string().nullable().optional(),
|
|
83
|
+
conclusion: z.string().nullable().optional(),
|
|
84
|
+
html_url: z.string().nullable().optional(),
|
|
85
|
+
details_url: z.string().nullable().optional(),
|
|
86
|
+
output: z
|
|
87
|
+
.object({
|
|
88
|
+
title: z.string().nullable().optional(),
|
|
89
|
+
summary: z.string().nullable().optional(),
|
|
90
|
+
text: z.string().nullable().optional(),
|
|
91
|
+
})
|
|
92
|
+
.nullable()
|
|
93
|
+
.optional(),
|
|
94
|
+
check_suite: z
|
|
95
|
+
.object({
|
|
96
|
+
workflow_run: z
|
|
97
|
+
.object({
|
|
98
|
+
id: z.number().nullable().optional(),
|
|
99
|
+
})
|
|
100
|
+
.nullable()
|
|
101
|
+
.optional(),
|
|
102
|
+
})
|
|
103
|
+
.nullable()
|
|
104
|
+
.optional(),
|
|
105
|
+
});
|
|
106
|
+
const GitHubCheckAnnotationSchema = z.object({
|
|
107
|
+
path: z.string().optional(),
|
|
108
|
+
start_line: z.number().optional(),
|
|
109
|
+
end_line: z.number().optional(),
|
|
110
|
+
annotation_level: z.string().optional(),
|
|
111
|
+
message: z.string().optional(),
|
|
112
|
+
title: z.string().optional(),
|
|
113
|
+
raw_details: z.string().optional(),
|
|
114
|
+
});
|
|
115
|
+
const GitHubCheckAnnotationsSchema = z.array(GitHubCheckAnnotationSchema).catch([]);
|
|
116
|
+
const GitHubActionsJobSchema = z.object({
|
|
117
|
+
id: z.number(),
|
|
118
|
+
name: z.string().catch(""),
|
|
119
|
+
status: z.string().nullable().optional(),
|
|
120
|
+
conclusion: z.string().nullable().optional(),
|
|
121
|
+
html_url: z.string().nullable().optional(),
|
|
122
|
+
completed_at: z.string().nullable().optional(),
|
|
123
|
+
});
|
|
124
|
+
const GitHubActionsJobsSchema = z.object({
|
|
125
|
+
jobs: z.array(GitHubActionsJobSchema).catch([]),
|
|
126
|
+
});
|
|
72
127
|
const PullRequestReviewDecisionSchema = z
|
|
73
128
|
.enum(["APPROVED", "CHANGES_REQUESTED", "REVIEW_REQUIRED"])
|
|
74
129
|
.nullable()
|
|
@@ -142,6 +197,7 @@ const TimelineAuthorSchema = z
|
|
|
142
197
|
.object({
|
|
143
198
|
login: z.string().optional(),
|
|
144
199
|
url: z.string().nullable().optional(),
|
|
200
|
+
avatarUrl: z.string().nullable().optional(),
|
|
145
201
|
})
|
|
146
202
|
.nullable()
|
|
147
203
|
.optional();
|
|
@@ -149,6 +205,7 @@ const PullRequestTimelineReviewNodeSchema = z.object({
|
|
|
149
205
|
id: z.string().catch(""),
|
|
150
206
|
state: z.string().catch(""),
|
|
151
207
|
body: z.string().nullable().catch(null),
|
|
208
|
+
bodyHTML: z.string().nullable().catch(null),
|
|
152
209
|
url: z.string().catch(""),
|
|
153
210
|
submittedAt: z.string().nullable().catch(null),
|
|
154
211
|
author: TimelineAuthorSchema,
|
|
@@ -156,10 +213,32 @@ const PullRequestTimelineReviewNodeSchema = z.object({
|
|
|
156
213
|
const PullRequestTimelineCommentNodeSchema = z.object({
|
|
157
214
|
id: z.string().catch(""),
|
|
158
215
|
body: z.string().nullable().catch(null),
|
|
216
|
+
bodyHTML: z.string().nullable().catch(null),
|
|
159
217
|
url: z.string().catch(""),
|
|
160
218
|
createdAt: z.string().nullable().catch(null),
|
|
161
219
|
author: TimelineAuthorSchema,
|
|
162
220
|
});
|
|
221
|
+
const PullRequestReviewThreadCommentNodeSchema = PullRequestTimelineCommentNodeSchema.extend({
|
|
222
|
+
pullRequestReview: z
|
|
223
|
+
.object({ id: z.string().catch("") })
|
|
224
|
+
.nullable()
|
|
225
|
+
.optional()
|
|
226
|
+
.catch(null),
|
|
227
|
+
});
|
|
228
|
+
const PullRequestReviewThreadNodeSchema = z.object({
|
|
229
|
+
id: z.string().catch(""),
|
|
230
|
+
path: z.string().catch(""),
|
|
231
|
+
line: z.number().nullable().optional().catch(null),
|
|
232
|
+
startLine: z.number().nullable().optional().catch(null),
|
|
233
|
+
isResolved: z.boolean().catch(false),
|
|
234
|
+
isOutdated: z.boolean().catch(false),
|
|
235
|
+
comments: z
|
|
236
|
+
.object({
|
|
237
|
+
nodes: z.array(PullRequestReviewThreadCommentNodeSchema).catch([]),
|
|
238
|
+
pageInfo: z.object({ hasNextPage: z.boolean().catch(false) }).catch({ hasNextPage: false }),
|
|
239
|
+
})
|
|
240
|
+
.catch({ nodes: [], pageInfo: { hasNextPage: false } }),
|
|
241
|
+
});
|
|
163
242
|
const PullRequestTimelinePageInfoSchema = z.object({
|
|
164
243
|
hasNextPage: z.boolean().catch(false),
|
|
165
244
|
});
|
|
@@ -183,6 +262,12 @@ const PullRequestTimelineGraphqlSchema = z.object({
|
|
|
183
262
|
pageInfo: PullRequestTimelinePageInfoSchema.catch({ hasNextPage: false }),
|
|
184
263
|
})
|
|
185
264
|
.catch({ nodes: [], pageInfo: { hasNextPage: false } }),
|
|
265
|
+
reviewThreads: z
|
|
266
|
+
.object({
|
|
267
|
+
nodes: z.array(PullRequestReviewThreadNodeSchema).catch([]),
|
|
268
|
+
pageInfo: PullRequestTimelinePageInfoSchema.catch({ hasNextPage: false }),
|
|
269
|
+
})
|
|
270
|
+
.catch({ nodes: [], pageInfo: { hasNextPage: false } }),
|
|
186
271
|
})
|
|
187
272
|
.nullable()
|
|
188
273
|
.optional(),
|
|
@@ -296,11 +381,13 @@ query PullRequestTimeline($owner: String!, $name: String!, $number: Int!) {
|
|
|
296
381
|
id
|
|
297
382
|
state
|
|
298
383
|
body
|
|
384
|
+
bodyHTML
|
|
299
385
|
url
|
|
300
386
|
submittedAt
|
|
301
387
|
author {
|
|
302
388
|
login
|
|
303
389
|
url
|
|
390
|
+
avatarUrl
|
|
304
391
|
}
|
|
305
392
|
}
|
|
306
393
|
pageInfo {
|
|
@@ -311,11 +398,46 @@ query PullRequestTimeline($owner: String!, $name: String!, $number: Int!) {
|
|
|
311
398
|
nodes {
|
|
312
399
|
id
|
|
313
400
|
body
|
|
401
|
+
bodyHTML
|
|
314
402
|
url
|
|
315
403
|
createdAt
|
|
316
404
|
author {
|
|
317
405
|
login
|
|
318
406
|
url
|
|
407
|
+
avatarUrl
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
pageInfo {
|
|
411
|
+
hasNextPage
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
reviewThreads(first: 100) {
|
|
415
|
+
nodes {
|
|
416
|
+
id
|
|
417
|
+
path
|
|
418
|
+
line
|
|
419
|
+
startLine
|
|
420
|
+
isResolved
|
|
421
|
+
isOutdated
|
|
422
|
+
comments(first: 100) {
|
|
423
|
+
nodes {
|
|
424
|
+
id
|
|
425
|
+
body
|
|
426
|
+
bodyHTML
|
|
427
|
+
url
|
|
428
|
+
createdAt
|
|
429
|
+
author {
|
|
430
|
+
login
|
|
431
|
+
url
|
|
432
|
+
avatarUrl
|
|
433
|
+
}
|
|
434
|
+
pullRequestReview {
|
|
435
|
+
id
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
pageInfo {
|
|
439
|
+
hasNextPage
|
|
440
|
+
}
|
|
319
441
|
}
|
|
320
442
|
}
|
|
321
443
|
pageInfo {
|
|
@@ -362,6 +484,7 @@ export function createGitHubService(options = {}) {
|
|
|
362
484
|
const cache = new Map();
|
|
363
485
|
const inFlight = new Map();
|
|
364
486
|
const pollTargets = new Map();
|
|
487
|
+
const checkLogTailCache = new Map();
|
|
365
488
|
let api;
|
|
366
489
|
async function cached(params) {
|
|
367
490
|
if (params.readOptions?.force && !params.readOptions.reason) {
|
|
@@ -651,6 +774,79 @@ export function createGitHubService(options = {}) {
|
|
|
651
774
|
},
|
|
652
775
|
});
|
|
653
776
|
},
|
|
777
|
+
getGitHubCheckDetails(input) {
|
|
778
|
+
return cached({
|
|
779
|
+
cwd: input.cwd,
|
|
780
|
+
method: "getGitHubCheckDetails",
|
|
781
|
+
args: {
|
|
782
|
+
repoOwner: input.repoOwner,
|
|
783
|
+
repoName: input.repoName,
|
|
784
|
+
checkRunId: input.checkRunId,
|
|
785
|
+
workflowRunId: input.workflowRunId,
|
|
786
|
+
},
|
|
787
|
+
readOptions: input,
|
|
788
|
+
load: async () => {
|
|
789
|
+
const repoPath = `repos/${input.repoOwner}/${input.repoName}`;
|
|
790
|
+
const checkRun = parseGitHubCheckRunDetails(await run(["api", `${repoPath}/check-runs/${input.checkRunId}`], { cwd: input.cwd }));
|
|
791
|
+
const annotations = parseGitHubCheckAnnotations(await run([
|
|
792
|
+
"api",
|
|
793
|
+
`${repoPath}/check-runs/${input.checkRunId}/annotations`,
|
|
794
|
+
"-f",
|
|
795
|
+
`per_page=${CHECK_ANNOTATION_PAGE_MAX}`,
|
|
796
|
+
], {
|
|
797
|
+
cwd: input.cwd,
|
|
798
|
+
}));
|
|
799
|
+
const workflowRunId = input.workflowRunId ?? checkRun.workflowRunId ?? null;
|
|
800
|
+
const failedJobs = [];
|
|
801
|
+
let truncated = annotations.length >= CHECK_ANNOTATION_PAGE_MAX;
|
|
802
|
+
if (typeof workflowRunId === "number") {
|
|
803
|
+
const jobs = parseGitHubActionsJobs(await run([
|
|
804
|
+
"api",
|
|
805
|
+
`${repoPath}/actions/runs/${workflowRunId}/jobs`,
|
|
806
|
+
"-f",
|
|
807
|
+
`per_page=${ACTIONS_JOB_PAGE_MAX}`,
|
|
808
|
+
], {
|
|
809
|
+
cwd: input.cwd,
|
|
810
|
+
}));
|
|
811
|
+
const failed = jobs.filter(isFailedActionsJob);
|
|
812
|
+
truncated || (truncated = jobs.length >= ACTIONS_JOB_PAGE_MAX);
|
|
813
|
+
truncated || (truncated = failed.length > FAILED_CHECK_JOB_LIMIT);
|
|
814
|
+
for (const job of failed.slice(0, FAILED_CHECK_JOB_LIMIT)) {
|
|
815
|
+
const log = await getCachedCheckLogTail({
|
|
816
|
+
cwd: input.cwd,
|
|
817
|
+
repoPath,
|
|
818
|
+
job,
|
|
819
|
+
run,
|
|
820
|
+
cache: checkLogTailCache,
|
|
821
|
+
});
|
|
822
|
+
truncated || (truncated = log.logTruncated);
|
|
823
|
+
failedJobs.push({
|
|
824
|
+
jobId: job.jobId,
|
|
825
|
+
name: job.name,
|
|
826
|
+
status: job.status,
|
|
827
|
+
conclusion: job.conclusion,
|
|
828
|
+
url: job.url,
|
|
829
|
+
logTail: log.logTail,
|
|
830
|
+
logTruncated: log.logTruncated,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
checkRunId: checkRun.checkRunId,
|
|
836
|
+
workflowRunId,
|
|
837
|
+
name: checkRun.name,
|
|
838
|
+
status: checkRun.status,
|
|
839
|
+
conclusion: checkRun.conclusion,
|
|
840
|
+
url: checkRun.url,
|
|
841
|
+
detailsUrl: checkRun.detailsUrl,
|
|
842
|
+
output: checkRun.output,
|
|
843
|
+
annotations,
|
|
844
|
+
failedJobs,
|
|
845
|
+
truncated,
|
|
846
|
+
};
|
|
847
|
+
},
|
|
848
|
+
});
|
|
849
|
+
},
|
|
654
850
|
async searchIssuesAndPrs(input) {
|
|
655
851
|
if (input.force && !input.reason) {
|
|
656
852
|
throw new Error("GitHubService forced read requires a reason");
|
|
@@ -1356,10 +1552,17 @@ function parseIssueSummaries(stdout) {
|
|
|
1356
1552
|
function parsePullRequestTimeline(stdout, identity) {
|
|
1357
1553
|
const parsed = PullRequestTimelineGraphqlSchema.parse(JSON.parse(stdout || "{}"));
|
|
1358
1554
|
const pullRequest = parsed.data?.repository?.pullRequest;
|
|
1555
|
+
const reviewThreadItems = pullRequest
|
|
1556
|
+
? pullRequest.reviewThreads.nodes.flatMap(toPullRequestTimelineReviewThreadItems)
|
|
1557
|
+
: [];
|
|
1558
|
+
const reviewThreadItemIds = new Set(reviewThreadItems.map((item) => item.id).filter((id) => id.length > 0));
|
|
1359
1559
|
const items = pullRequest
|
|
1360
1560
|
? [
|
|
1361
1561
|
...pullRequest.reviews.nodes.flatMap(toPullRequestTimelineReviewItem),
|
|
1362
|
-
...pullRequest.comments.nodes
|
|
1562
|
+
...pullRequest.comments.nodes
|
|
1563
|
+
.filter((comment) => !reviewThreadItemIds.has(comment.id))
|
|
1564
|
+
.map(toPullRequestTimelineCommentItem),
|
|
1565
|
+
...reviewThreadItems,
|
|
1363
1566
|
].sort(compareTimelineItems)
|
|
1364
1567
|
: [];
|
|
1365
1568
|
return {
|
|
@@ -1367,8 +1570,11 @@ function parsePullRequestTimeline(stdout, identity) {
|
|
|
1367
1570
|
repoOwner: identity.repoOwner,
|
|
1368
1571
|
repoName: identity.repoName,
|
|
1369
1572
|
items,
|
|
1370
|
-
// S3 deliberately caps timeline fetches at the first 100 reviews and
|
|
1371
|
-
truncated: Boolean(pullRequest?.reviews.pageInfo.hasNextPage ||
|
|
1573
|
+
// S3 deliberately caps timeline fetches at the first 100 reviews, comments, and review threads.
|
|
1574
|
+
truncated: Boolean(pullRequest?.reviews.pageInfo.hasNextPage ||
|
|
1575
|
+
pullRequest?.comments.pageInfo.hasNextPage ||
|
|
1576
|
+
pullRequest?.reviewThreads.pageInfo.hasNextPage ||
|
|
1577
|
+
pullRequest?.reviewThreads.nodes.some((thread) => thread.comments.pageInfo.hasNextPage)),
|
|
1372
1578
|
error: pullRequest ? null : { kind: "not_found", message: "Pull request not found" },
|
|
1373
1579
|
};
|
|
1374
1580
|
}
|
|
@@ -1383,7 +1589,8 @@ function toPullRequestTimelineReviewItem(review) {
|
|
|
1383
1589
|
id: review.id,
|
|
1384
1590
|
author: review.author?.login ?? "unknown",
|
|
1385
1591
|
authorUrl: review.author?.url ?? null,
|
|
1386
|
-
|
|
1592
|
+
avatarUrl: review.author?.avatarUrl ?? null,
|
|
1593
|
+
body: normalizeGitHubTimelineBody(review.body ?? "", review.bodyHTML ?? ""),
|
|
1387
1594
|
createdAt: parseOptionalTime(review.submittedAt ?? null),
|
|
1388
1595
|
url: review.url,
|
|
1389
1596
|
reviewState,
|
|
@@ -1396,11 +1603,229 @@ function toPullRequestTimelineCommentItem(comment) {
|
|
|
1396
1603
|
id: comment.id,
|
|
1397
1604
|
author: comment.author?.login ?? "unknown",
|
|
1398
1605
|
authorUrl: comment.author?.url ?? null,
|
|
1399
|
-
|
|
1606
|
+
avatarUrl: comment.author?.avatarUrl ?? null,
|
|
1607
|
+
body: normalizeGitHubTimelineBody(comment.body ?? "", comment.bodyHTML ?? ""),
|
|
1400
1608
|
createdAt: parseOptionalTime(comment.createdAt ?? null),
|
|
1401
1609
|
url: comment.url,
|
|
1402
1610
|
};
|
|
1403
1611
|
}
|
|
1612
|
+
const RAW_MARKDOWN_IMAGE_RE = /!\[[^\]]*\]\(\s*([^\s)]+)(?:\s+["'][^)]*["'])?\s*\)/g;
|
|
1613
|
+
const HTML_IMAGE_RE = /<img\b[^>]*\bsrc\s*=\s*(["'])(.*?)\1[^>]*>/gi;
|
|
1614
|
+
const GITHUB_RENDERED_IMAGE_HOSTS = new Set([
|
|
1615
|
+
"camo.githubusercontent.com",
|
|
1616
|
+
"private-user-images.githubusercontent.com",
|
|
1617
|
+
]);
|
|
1618
|
+
function normalizeGitHubTimelineBody(body, bodyHTML) {
|
|
1619
|
+
const rawImages = extractRawImageSourceReferences(body);
|
|
1620
|
+
if (rawImages.length === 0) {
|
|
1621
|
+
return body;
|
|
1622
|
+
}
|
|
1623
|
+
const renderedSources = extractRenderedImageSources(bodyHTML);
|
|
1624
|
+
if (renderedSources.length !== rawImages.length) {
|
|
1625
|
+
return body;
|
|
1626
|
+
}
|
|
1627
|
+
let cursor = 0;
|
|
1628
|
+
let normalized = "";
|
|
1629
|
+
for (let index = 0; index < rawImages.length; index += 1) {
|
|
1630
|
+
const rawImage = rawImages[index];
|
|
1631
|
+
const renderedSrc = renderedSources[index];
|
|
1632
|
+
if (!rawImage ||
|
|
1633
|
+
!renderedSrc ||
|
|
1634
|
+
!isRawGitHubAttachmentSource(rawImage.src) ||
|
|
1635
|
+
!isGitHubRenderedImageSource(renderedSrc)) {
|
|
1636
|
+
return body;
|
|
1637
|
+
}
|
|
1638
|
+
normalized += body.slice(cursor, rawImage.start);
|
|
1639
|
+
normalized += renderedSrc;
|
|
1640
|
+
cursor = rawImage.end;
|
|
1641
|
+
}
|
|
1642
|
+
normalized += body.slice(cursor);
|
|
1643
|
+
return normalized;
|
|
1644
|
+
}
|
|
1645
|
+
function extractRawImageSourceReferences(source) {
|
|
1646
|
+
const references = [
|
|
1647
|
+
...extractHtmlImageSourceReferences(source),
|
|
1648
|
+
...extractMarkdownImageSourceReferences(source),
|
|
1649
|
+
];
|
|
1650
|
+
return references.sort((left, right) => left.start - right.start);
|
|
1651
|
+
}
|
|
1652
|
+
function extractRenderedImageSources(source) {
|
|
1653
|
+
return extractHtmlImageSourceReferences(source).map((reference) => reference.src);
|
|
1654
|
+
}
|
|
1655
|
+
function extractHtmlImageSourceReferences(source) {
|
|
1656
|
+
const references = [];
|
|
1657
|
+
HTML_IMAGE_RE.lastIndex = 0;
|
|
1658
|
+
let match;
|
|
1659
|
+
while ((match = HTML_IMAGE_RE.exec(source)) !== null) {
|
|
1660
|
+
const src = decodeHtmlAttribute(match[2] ?? "");
|
|
1661
|
+
if (!src) {
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
const rawAttributeSrc = match[2] ?? "";
|
|
1665
|
+
const start = match.index + match[0].indexOf(rawAttributeSrc);
|
|
1666
|
+
references.push({ src, start, end: start + rawAttributeSrc.length });
|
|
1667
|
+
}
|
|
1668
|
+
return references;
|
|
1669
|
+
}
|
|
1670
|
+
function extractMarkdownImageSourceReferences(source) {
|
|
1671
|
+
const references = [];
|
|
1672
|
+
RAW_MARKDOWN_IMAGE_RE.lastIndex = 0;
|
|
1673
|
+
let match;
|
|
1674
|
+
while ((match = RAW_MARKDOWN_IMAGE_RE.exec(source)) !== null) {
|
|
1675
|
+
const src = match[1] ?? "";
|
|
1676
|
+
if (!src) {
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
const start = match.index + match[0].indexOf(src);
|
|
1680
|
+
references.push({ src, start, end: start + src.length });
|
|
1681
|
+
}
|
|
1682
|
+
return references;
|
|
1683
|
+
}
|
|
1684
|
+
function isRawGitHubAttachmentSource(src) {
|
|
1685
|
+
try {
|
|
1686
|
+
const url = new URL(src);
|
|
1687
|
+
return (url.protocol === "https:" &&
|
|
1688
|
+
url.hostname === "github.com" &&
|
|
1689
|
+
url.pathname.startsWith("/user-attachments/assets/"));
|
|
1690
|
+
}
|
|
1691
|
+
catch {
|
|
1692
|
+
return false;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
function isGitHubRenderedImageSource(src) {
|
|
1696
|
+
try {
|
|
1697
|
+
const url = new URL(src);
|
|
1698
|
+
return url.protocol === "https:" && GITHUB_RENDERED_IMAGE_HOSTS.has(url.hostname);
|
|
1699
|
+
}
|
|
1700
|
+
catch {
|
|
1701
|
+
return false;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
function decodeHtmlAttribute(value) {
|
|
1705
|
+
return value
|
|
1706
|
+
.replaceAll("&", "&")
|
|
1707
|
+
.replaceAll(""", '"')
|
|
1708
|
+
.replaceAll("'", "'")
|
|
1709
|
+
.replaceAll("<", "<")
|
|
1710
|
+
.replaceAll(">", ">");
|
|
1711
|
+
}
|
|
1712
|
+
function toPullRequestTimelineReviewThreadItems(thread) {
|
|
1713
|
+
return thread.comments.nodes.map((comment) => ({
|
|
1714
|
+
...toPullRequestTimelineCommentItem(comment),
|
|
1715
|
+
...(comment.pullRequestReview?.id ? { reviewId: comment.pullRequestReview.id } : {}),
|
|
1716
|
+
location: {
|
|
1717
|
+
path: thread.path,
|
|
1718
|
+
...(thread.line !== null && thread.line !== undefined ? { line: thread.line } : {}),
|
|
1719
|
+
...(thread.startLine !== null && thread.startLine !== undefined
|
|
1720
|
+
? { startLine: thread.startLine }
|
|
1721
|
+
: {}),
|
|
1722
|
+
...(thread.id ? { threadId: thread.id } : {}),
|
|
1723
|
+
isResolved: thread.isResolved,
|
|
1724
|
+
isOutdated: thread.isOutdated,
|
|
1725
|
+
},
|
|
1726
|
+
}));
|
|
1727
|
+
}
|
|
1728
|
+
function parseGitHubCheckRunDetails(stdout) {
|
|
1729
|
+
const parsed = GitHubCheckRunDetailsSchema.parse(JSON.parse(stdout || "{}"));
|
|
1730
|
+
return {
|
|
1731
|
+
checkRunId: parsed.id,
|
|
1732
|
+
workflowRunId: parsed.check_suite?.workflow_run?.id ?? null,
|
|
1733
|
+
name: parsed.name,
|
|
1734
|
+
status: parsed.status,
|
|
1735
|
+
conclusion: parsed.conclusion,
|
|
1736
|
+
url: parsed.html_url,
|
|
1737
|
+
detailsUrl: parsed.details_url,
|
|
1738
|
+
output: parsed.output,
|
|
1739
|
+
annotations: [],
|
|
1740
|
+
failedJobs: [],
|
|
1741
|
+
truncated: false,
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
function parseGitHubCheckAnnotations(stdout) {
|
|
1745
|
+
return GitHubCheckAnnotationsSchema.parse(JSON.parse(stdout || "[]")).map((annotation) => {
|
|
1746
|
+
const result = {};
|
|
1747
|
+
if (annotation.path)
|
|
1748
|
+
result.path = annotation.path;
|
|
1749
|
+
if (annotation.start_line !== undefined)
|
|
1750
|
+
result.startLine = annotation.start_line;
|
|
1751
|
+
if (annotation.end_line !== undefined)
|
|
1752
|
+
result.endLine = annotation.end_line;
|
|
1753
|
+
if (annotation.annotation_level)
|
|
1754
|
+
result.annotationLevel = annotation.annotation_level;
|
|
1755
|
+
if (annotation.message)
|
|
1756
|
+
result.message = annotation.message;
|
|
1757
|
+
if (annotation.title)
|
|
1758
|
+
result.title = annotation.title;
|
|
1759
|
+
if (annotation.raw_details)
|
|
1760
|
+
result.rawDetails = annotation.raw_details;
|
|
1761
|
+
return result;
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
function parseGitHubActionsJobs(stdout) {
|
|
1765
|
+
return GitHubActionsJobsSchema.parse(JSON.parse(stdout || "{}")).jobs.map((job) => {
|
|
1766
|
+
const result = {
|
|
1767
|
+
jobId: job.id,
|
|
1768
|
+
name: job.name,
|
|
1769
|
+
status: job.status,
|
|
1770
|
+
conclusion: job.conclusion,
|
|
1771
|
+
url: job.html_url,
|
|
1772
|
+
};
|
|
1773
|
+
if (job.completed_at)
|
|
1774
|
+
result.completedAt = job.completed_at;
|
|
1775
|
+
return result;
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
function isFailedActionsJob(job) {
|
|
1779
|
+
return (job.conclusion === "failure" ||
|
|
1780
|
+
job.conclusion === "cancelled" ||
|
|
1781
|
+
job.conclusion === "timed_out" ||
|
|
1782
|
+
job.conclusion === "action_required");
|
|
1783
|
+
}
|
|
1784
|
+
async function getCachedCheckLogTail(input) {
|
|
1785
|
+
const key = `${input.job.jobId}:${input.job.completedAt ?? ""}`;
|
|
1786
|
+
const cached = input.cache.get(key);
|
|
1787
|
+
if (cached) {
|
|
1788
|
+
input.cache.delete(key);
|
|
1789
|
+
input.cache.set(key, cached);
|
|
1790
|
+
return cached;
|
|
1791
|
+
}
|
|
1792
|
+
const capped = capCheckLogTail(await input.run(["api", `${input.repoPath}/actions/jobs/${input.job.jobId}/logs`], {
|
|
1793
|
+
cwd: input.cwd,
|
|
1794
|
+
}));
|
|
1795
|
+
input.cache.set(key, capped);
|
|
1796
|
+
while (input.cache.size > CHECK_LOG_TAIL_CACHE_MAX_ENTRIES) {
|
|
1797
|
+
const oldestKey = input.cache.keys().next().value;
|
|
1798
|
+
if (!oldestKey) {
|
|
1799
|
+
break;
|
|
1800
|
+
}
|
|
1801
|
+
input.cache.delete(oldestKey);
|
|
1802
|
+
}
|
|
1803
|
+
return capped;
|
|
1804
|
+
}
|
|
1805
|
+
function capCheckLogTail(log) {
|
|
1806
|
+
const lines = log.split("\n");
|
|
1807
|
+
let truncated = lines.length > CHECK_LOG_TAIL_MAX_LINES;
|
|
1808
|
+
let tail = lines.slice(-CHECK_LOG_TAIL_MAX_LINES).join("\n");
|
|
1809
|
+
if (Buffer.byteLength(tail, "utf8") > CHECK_LOG_TAIL_MAX_BYTES) {
|
|
1810
|
+
truncated = true;
|
|
1811
|
+
tail = utf8SuffixWithinBytes(tail, CHECK_LOG_TAIL_MAX_BYTES);
|
|
1812
|
+
}
|
|
1813
|
+
return { logTail: tail, logTruncated: truncated };
|
|
1814
|
+
}
|
|
1815
|
+
function utf8SuffixWithinBytes(value, maxBytes) {
|
|
1816
|
+
let lowerBound = 0;
|
|
1817
|
+
let upperBound = value.length;
|
|
1818
|
+
while (lowerBound < upperBound) {
|
|
1819
|
+
const midpoint = Math.floor((lowerBound + upperBound) / 2);
|
|
1820
|
+
if (Buffer.byteLength(value.slice(midpoint), "utf8") > maxBytes) {
|
|
1821
|
+
lowerBound = midpoint + 1;
|
|
1822
|
+
}
|
|
1823
|
+
else {
|
|
1824
|
+
upperBound = midpoint;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
return value.slice(lowerBound);
|
|
1828
|
+
}
|
|
1404
1829
|
function mapTimelineReviewState(state, body) {
|
|
1405
1830
|
switch (state) {
|
|
1406
1831
|
case "APPROVED":
|
|
@@ -1541,6 +1966,10 @@ function buildPullRequestCheck(context) {
|
|
|
1541
1966
|
...(typeof context.workflowName === "string" && context.workflowName.trim().length > 0
|
|
1542
1967
|
? { workflow: context.workflowName }
|
|
1543
1968
|
: {}),
|
|
1969
|
+
...(typeof context.databaseId === "number" ? { checkRunId: context.databaseId } : {}),
|
|
1970
|
+
...(typeof context.checkSuite?.workflowRun?.databaseId === "number"
|
|
1971
|
+
? { workflowRunId: context.checkSuite.workflowRun.databaseId }
|
|
1972
|
+
: {}),
|
|
1544
1973
|
...formatCheckRunDuration(context),
|
|
1545
1974
|
recency: getCheckRunRecency(context),
|
|
1546
1975
|
};
|
|
@@ -9,6 +9,7 @@ export interface TerminalSessionControllerOptions {
|
|
|
9
9
|
hasBinaryChannel: () => boolean;
|
|
10
10
|
isPathWithinRoot: (rootPath: string, candidatePath: string) => boolean;
|
|
11
11
|
sessionLogger: pino.Logger;
|
|
12
|
+
listTerminalWorkspaceRoots?: () => Promise<readonly string[]>;
|
|
12
13
|
clientSupportsWrapReflow?: () => boolean;
|
|
13
14
|
}
|
|
14
15
|
export interface TerminalSessionControllerMetrics {
|
|
@@ -22,6 +23,7 @@ export declare class TerminalSessionController {
|
|
|
22
23
|
private readonly hasBinaryChannel;
|
|
23
24
|
private readonly isPathWithinRoot;
|
|
24
25
|
private readonly sessionLogger;
|
|
26
|
+
private readonly listTerminalWorkspaceRoots;
|
|
25
27
|
private readonly clientSupportsWrapReflow;
|
|
26
28
|
private readonly subscribedDirectories;
|
|
27
29
|
private unsubscribeTerminalsChanged;
|
|
@@ -50,6 +52,10 @@ export declare class TerminalSessionController {
|
|
|
50
52
|
private emitTerminalsSnapshotForRoot;
|
|
51
53
|
private handleListTerminalsRequest;
|
|
52
54
|
private getAllTerminalSessions;
|
|
55
|
+
private getTerminalsForWorkspaceRoot;
|
|
56
|
+
private terminalBelongsToRoot;
|
|
57
|
+
private resolveTerminalOwnerRoot;
|
|
58
|
+
private isSamePath;
|
|
53
59
|
private handleCreateTerminalRequest;
|
|
54
60
|
private handleRenameTerminalRequest;
|
|
55
61
|
private handleSubscribeTerminalRequest;
|
|
@@ -29,6 +29,7 @@ export class TerminalSessionController {
|
|
|
29
29
|
this.hasBinaryChannel = options.hasBinaryChannel;
|
|
30
30
|
this.isPathWithinRoot = options.isPathWithinRoot;
|
|
31
31
|
this.sessionLogger = options.sessionLogger;
|
|
32
|
+
this.listTerminalWorkspaceRoots = options.listTerminalWorkspaceRoots ?? (async () => []);
|
|
32
33
|
this.clientSupportsWrapReflow = options.clientSupportsWrapReflow ?? (() => false);
|
|
33
34
|
}
|
|
34
35
|
start() {
|
|
@@ -199,7 +200,7 @@ export class TerminalSessionController {
|
|
|
199
200
|
return;
|
|
200
201
|
}
|
|
201
202
|
try {
|
|
202
|
-
const terminals = await this.
|
|
203
|
+
const terminals = await this.getTerminalsForWorkspaceRoot(cwd);
|
|
203
204
|
for (const terminal of terminals) {
|
|
204
205
|
this.ensureExitSubscription(terminal);
|
|
205
206
|
}
|
|
@@ -229,7 +230,7 @@ export class TerminalSessionController {
|
|
|
229
230
|
}
|
|
230
231
|
try {
|
|
231
232
|
const terminals = typeof msg.cwd === "string"
|
|
232
|
-
? await this.
|
|
233
|
+
? await this.getTerminalsForWorkspaceRoot(msg.cwd)
|
|
233
234
|
: await this.getAllTerminalSessions();
|
|
234
235
|
for (const terminal of terminals) {
|
|
235
236
|
this.ensureExitSubscription(terminal);
|
|
@@ -264,6 +265,39 @@ export class TerminalSessionController {
|
|
|
264
265
|
const terminalsByDirectory = await Promise.all(directories.map((cwd) => manager.getTerminals(cwd)));
|
|
265
266
|
return terminalsByDirectory.flat();
|
|
266
267
|
}
|
|
268
|
+
async getTerminalsForWorkspaceRoot(cwd) {
|
|
269
|
+
if (!this.terminalManager) {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
const terminals = await this.terminalManager.getTerminals(cwd);
|
|
273
|
+
const workspaceRoots = await this.listTerminalWorkspaceRoots();
|
|
274
|
+
if (workspaceRoots.length === 0) {
|
|
275
|
+
return terminals;
|
|
276
|
+
}
|
|
277
|
+
return terminals.filter((terminal) => this.terminalBelongsToRoot(cwd, terminal.cwd, workspaceRoots));
|
|
278
|
+
}
|
|
279
|
+
terminalBelongsToRoot(rootCwd, terminalCwd, workspaceRoots) {
|
|
280
|
+
const ownerRoot = this.resolveTerminalOwnerRoot(terminalCwd, workspaceRoots);
|
|
281
|
+
if (!ownerRoot) {
|
|
282
|
+
return this.isPathWithinRoot(rootCwd, terminalCwd);
|
|
283
|
+
}
|
|
284
|
+
return this.isSamePath(rootCwd, ownerRoot);
|
|
285
|
+
}
|
|
286
|
+
resolveTerminalOwnerRoot(terminalCwd, workspaceRoots) {
|
|
287
|
+
let ownerRoot = null;
|
|
288
|
+
for (const workspaceRoot of workspaceRoots) {
|
|
289
|
+
if (!this.isPathWithinRoot(workspaceRoot, terminalCwd)) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (!ownerRoot || workspaceRoot.length > ownerRoot.length) {
|
|
293
|
+
ownerRoot = workspaceRoot;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return ownerRoot;
|
|
297
|
+
}
|
|
298
|
+
isSamePath(firstPath, secondPath) {
|
|
299
|
+
return (this.isPathWithinRoot(firstPath, secondPath) && this.isPathWithinRoot(secondPath, firstPath));
|
|
300
|
+
}
|
|
267
301
|
async handleCreateTerminalRequest(msg) {
|
|
268
302
|
if (!this.terminalManager) {
|
|
269
303
|
this.emit({
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { AgentProviderRuntimeSettingsMapSchema, migrateProviderSettings, ProviderOverridesSchema, } from "./agent/provider-launch-config.js";
|
|
5
5
|
import { ensurePrivateFile, writePrivateFileAtomicSync } from "./private-files.js";
|
|
6
|
+
import { TerminalProfileSchema } from "@getpaseo/protocol/messages";
|
|
6
7
|
export const LogLevelSchema = z.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
|
|
7
8
|
export const LogFormatSchema = z.enum(["pretty", "json"]);
|
|
8
9
|
const LogConfigSchema = z
|
|
@@ -187,6 +188,7 @@ export const PersistedConfigSchema = z
|
|
|
187
188
|
.optional(),
|
|
188
189
|
autoArchiveAfterMerge: z.boolean().optional(),
|
|
189
190
|
appendSystemPrompt: z.string().optional(),
|
|
191
|
+
terminalProfiles: z.array(TerminalProfileSchema).optional(),
|
|
190
192
|
cors: z
|
|
191
193
|
.object({
|
|
192
194
|
allowedOrigins: z.array(z.string()).optional(),
|