@opencode-linear-agent/server 0.1.3-master.11 → 0.1.3-master.13
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/index.js +493 -45
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -66849,6 +66849,16 @@ function getOpencodeErrorMessage(error) {
|
|
|
66849
66849
|
return JSON.stringify(error);
|
|
66850
66850
|
}
|
|
66851
66851
|
// ../core/src/linear/LinearServiceImpl.ts
|
|
66852
|
+
async function collectTeamLabels(team) {
|
|
66853
|
+
const first = await team.labels();
|
|
66854
|
+
const labels = [...first.nodes];
|
|
66855
|
+
let page = first;
|
|
66856
|
+
while (page.pageInfo.hasNextPage) {
|
|
66857
|
+
page = await page.fetchNext();
|
|
66858
|
+
labels.push(...page.nodes);
|
|
66859
|
+
}
|
|
66860
|
+
return labels;
|
|
66861
|
+
}
|
|
66852
66862
|
async function collectIssueAgentSessionIds(fetchPage) {
|
|
66853
66863
|
const ids = new Set;
|
|
66854
66864
|
let after;
|
|
@@ -67102,6 +67112,92 @@ ${truncatedStack}
|
|
|
67102
67112
|
}
|
|
67103
67113
|
return result;
|
|
67104
67114
|
}
|
|
67115
|
+
async getIssueRepositorySuggestions(issueId, agentSessionId, candidates) {
|
|
67116
|
+
const result = await Result.tryPromise({
|
|
67117
|
+
try: async () => {
|
|
67118
|
+
const response = await this.client.client.rawRequest(`query IssueRepositorySuggestions($issueId: String!, $agentSessionId: String!, $candidateRepositories: [RepositoryInput!]!) {
|
|
67119
|
+
issueRepositorySuggestions(
|
|
67120
|
+
issueId: $issueId
|
|
67121
|
+
agentSessionId: $agentSessionId
|
|
67122
|
+
candidateRepositories: $candidateRepositories
|
|
67123
|
+
) {
|
|
67124
|
+
suggestions {
|
|
67125
|
+
repositoryFullName
|
|
67126
|
+
hostname
|
|
67127
|
+
confidence
|
|
67128
|
+
}
|
|
67129
|
+
}
|
|
67130
|
+
}`, {
|
|
67131
|
+
issueId,
|
|
67132
|
+
agentSessionId,
|
|
67133
|
+
candidateRepositories: candidates
|
|
67134
|
+
});
|
|
67135
|
+
if (!response.data) {
|
|
67136
|
+
return [];
|
|
67137
|
+
}
|
|
67138
|
+
return response.data.issueRepositorySuggestions.suggestions;
|
|
67139
|
+
},
|
|
67140
|
+
catch: mapLinearError
|
|
67141
|
+
});
|
|
67142
|
+
if (Result.isError(result)) {
|
|
67143
|
+
this.log.error("Failed to get issue repository suggestions", {
|
|
67144
|
+
issueId,
|
|
67145
|
+
agentSessionId,
|
|
67146
|
+
candidateCount: candidates.length,
|
|
67147
|
+
error: result.error.message,
|
|
67148
|
+
errorType: result.error._tag
|
|
67149
|
+
});
|
|
67150
|
+
}
|
|
67151
|
+
return result;
|
|
67152
|
+
}
|
|
67153
|
+
async setIssueRepoLabel(issueId, labelName) {
|
|
67154
|
+
const result = await Result.tryPromise({
|
|
67155
|
+
try: async () => {
|
|
67156
|
+
const issue = await this.client.issue(issueId);
|
|
67157
|
+
const team = await issue.team;
|
|
67158
|
+
if (!team) {
|
|
67159
|
+
throw new Error("Issue has no associated team");
|
|
67160
|
+
}
|
|
67161
|
+
const issueLabels = await issue.labels();
|
|
67162
|
+
const teamLabels = await collectTeamLabels(team);
|
|
67163
|
+
let repoLabelId = teamLabels.find((label) => label.name.toLowerCase() === labelName.toLowerCase())?.id;
|
|
67164
|
+
if (!repoLabelId) {
|
|
67165
|
+
const response = await this.client.client.rawRequest(`mutation IssueLabelCreate($input: IssueLabelCreateInput!) {
|
|
67166
|
+
issueLabelCreate(input: $input) {
|
|
67167
|
+
success
|
|
67168
|
+
issueLabel {
|
|
67169
|
+
id
|
|
67170
|
+
}
|
|
67171
|
+
}
|
|
67172
|
+
}`, {
|
|
67173
|
+
input: {
|
|
67174
|
+
name: labelName,
|
|
67175
|
+
teamId: team.id
|
|
67176
|
+
}
|
|
67177
|
+
});
|
|
67178
|
+
repoLabelId = response.data?.issueLabelCreate.issueLabel?.id;
|
|
67179
|
+
if (!repoLabelId) {
|
|
67180
|
+
throw new Error(`Failed to create repo label ${labelName}`);
|
|
67181
|
+
}
|
|
67182
|
+
}
|
|
67183
|
+
const labelIds = issueLabels.nodes.filter((label) => !label.name.startsWith("repo:")).map((label) => label.id);
|
|
67184
|
+
await issue.update({
|
|
67185
|
+
labelIds: [...labelIds, repoLabelId]
|
|
67186
|
+
});
|
|
67187
|
+
},
|
|
67188
|
+
catch: mapLinearError
|
|
67189
|
+
});
|
|
67190
|
+
if (Result.isError(result)) {
|
|
67191
|
+
this.log.error("Failed to set repo label on issue", {
|
|
67192
|
+
issueId,
|
|
67193
|
+
labelName,
|
|
67194
|
+
error: result.error.message,
|
|
67195
|
+
errorType: result.error._tag
|
|
67196
|
+
});
|
|
67197
|
+
return Result.err(result.error);
|
|
67198
|
+
}
|
|
67199
|
+
return Result.ok(undefined);
|
|
67200
|
+
}
|
|
67105
67201
|
async getIssueAgentSessionIds(issueId) {
|
|
67106
67202
|
const result = await Result.tryPromise({
|
|
67107
67203
|
try: async () => {
|
|
@@ -67181,29 +67277,44 @@ ${truncatedStack}
|
|
|
67181
67277
|
}
|
|
67182
67278
|
}
|
|
67183
67279
|
// ../core/src/linear/label-parser.ts
|
|
67184
|
-
function
|
|
67280
|
+
function findRepoLabel(labels) {
|
|
67185
67281
|
const repoLabel = labels.find((label) => label.name.startsWith("repo:"));
|
|
67186
67282
|
if (!repoLabel) {
|
|
67187
|
-
return
|
|
67283
|
+
return { status: "missing" };
|
|
67188
67284
|
}
|
|
67189
67285
|
const repoPath = repoLabel.name.slice(5);
|
|
67190
67286
|
if (!repoPath.trim()) {
|
|
67191
|
-
return
|
|
67287
|
+
return { status: "invalid", label: repoLabel.name };
|
|
67192
67288
|
}
|
|
67193
67289
|
if (repoPath.includes("/")) {
|
|
67194
67290
|
const [organizationName, repositoryName] = repoPath.split("/", 2);
|
|
67195
67291
|
if (!organizationName?.trim() || !repositoryName?.trim()) {
|
|
67196
|
-
return
|
|
67292
|
+
return { status: "invalid", label: repoLabel.name };
|
|
67197
67293
|
}
|
|
67198
67294
|
return {
|
|
67199
|
-
|
|
67200
|
-
|
|
67295
|
+
status: "valid",
|
|
67296
|
+
label: repoLabel.name,
|
|
67297
|
+
value: {
|
|
67298
|
+
organizationName: organizationName.trim(),
|
|
67299
|
+
repositoryName: repositoryName.trim()
|
|
67300
|
+
}
|
|
67201
67301
|
};
|
|
67202
67302
|
}
|
|
67203
67303
|
return {
|
|
67204
|
-
|
|
67304
|
+
status: "valid",
|
|
67305
|
+
label: repoLabel.name,
|
|
67306
|
+
value: {
|
|
67307
|
+
repositoryName: repoPath.trim()
|
|
67308
|
+
}
|
|
67205
67309
|
};
|
|
67206
67310
|
}
|
|
67311
|
+
function parseRepoLabel(labels) {
|
|
67312
|
+
const result = findRepoLabel(labels);
|
|
67313
|
+
if (result.status !== "valid") {
|
|
67314
|
+
return null;
|
|
67315
|
+
}
|
|
67316
|
+
return result.value;
|
|
67317
|
+
}
|
|
67207
67318
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
67208
67319
|
var exports_external = {};
|
|
67209
67320
|
__export(exports_external, {
|
|
@@ -81389,6 +81500,7 @@ class FileTokenStore {
|
|
|
81389
81500
|
var SESSION_PREFIX = "session:";
|
|
81390
81501
|
var QUESTION_PREFIX = "question:";
|
|
81391
81502
|
var PERMISSION_PREFIX = "permission:";
|
|
81503
|
+
var REPO_SELECTION_PREFIX = "repo-selection:";
|
|
81392
81504
|
|
|
81393
81505
|
class FileSessionRepository {
|
|
81394
81506
|
kv;
|
|
@@ -81422,26 +81534,377 @@ class FileSessionRepository {
|
|
|
81422
81534
|
async deletePendingPermission(linearSessionId) {
|
|
81423
81535
|
await this.kv.delete(`${PERMISSION_PREFIX}${linearSessionId}`);
|
|
81424
81536
|
}
|
|
81537
|
+
async getPendingRepoSelection(linearSessionId) {
|
|
81538
|
+
return this.kv.get(`${REPO_SELECTION_PREFIX}${linearSessionId}`);
|
|
81539
|
+
}
|
|
81540
|
+
async savePendingRepoSelection(selection) {
|
|
81541
|
+
await this.kv.put(`${REPO_SELECTION_PREFIX}${selection.linearSessionId}`, selection);
|
|
81542
|
+
}
|
|
81543
|
+
async deletePendingRepoSelection(linearSessionId) {
|
|
81544
|
+
await this.kv.delete(`${REPO_SELECTION_PREFIX}${linearSessionId}`);
|
|
81545
|
+
}
|
|
81425
81546
|
}
|
|
81426
81547
|
// src/RepoResolver.ts
|
|
81427
81548
|
import { join as join3 } from "path";
|
|
81428
|
-
|
|
81549
|
+
import { readdir } from "fs/promises";
|
|
81550
|
+
function toRepoLabelSuggestion(candidate, confidence) {
|
|
81551
|
+
const parts = candidate.repositoryFullName.split("/");
|
|
81552
|
+
const repositoryName = parts[parts.length - 1] ?? candidate.repositoryFullName;
|
|
81553
|
+
return {
|
|
81554
|
+
confidence,
|
|
81555
|
+
hostname: candidate.hostname,
|
|
81556
|
+
labelValue: `repo:${candidate.repositoryFullName}`,
|
|
81557
|
+
repositoryFullName: candidate.repositoryFullName,
|
|
81558
|
+
repositoryName
|
|
81559
|
+
};
|
|
81560
|
+
}
|
|
81561
|
+
async function getCandidateRepositories(projectsPath) {
|
|
81562
|
+
const result = await Result.tryPromise({
|
|
81563
|
+
try: async () => readdir(projectsPath, { withFileTypes: true }),
|
|
81564
|
+
catch: (e) => e instanceof Error ? e.message : String(e)
|
|
81565
|
+
});
|
|
81566
|
+
if (Result.isError(result)) {
|
|
81567
|
+
return [];
|
|
81568
|
+
}
|
|
81569
|
+
return result.value.filter((entry) => entry.isDirectory()).map((entry) => ({
|
|
81570
|
+
hostname: "github.com",
|
|
81571
|
+
repositoryFullName: entry.name
|
|
81572
|
+
}));
|
|
81573
|
+
}
|
|
81574
|
+
async function resolveRepoPath(linear2, issueId, agentSessionId, projectsPath) {
|
|
81429
81575
|
const log = Log.create({ service: "repo-resolver" }).tag("issueId", issueId);
|
|
81430
81576
|
const labelsResult = await linear2.getIssueLabels(issueId);
|
|
81431
81577
|
if (Result.isError(labelsResult)) {
|
|
81432
81578
|
return Result.err(labelsResult.error);
|
|
81433
81579
|
}
|
|
81434
|
-
const
|
|
81435
|
-
|
|
81436
|
-
|
|
81437
|
-
|
|
81580
|
+
const parsedRepoLabel = parseRepoLabel(labelsResult.value);
|
|
81581
|
+
const repoLabel = findRepoLabel(labelsResult.value);
|
|
81582
|
+
if (parsedRepoLabel) {
|
|
81583
|
+
const repoPath = join3(projectsPath, parsedRepoLabel.repositoryName);
|
|
81584
|
+
log.info("Resolved repo from label", {
|
|
81585
|
+
repoName: parsedRepoLabel.repositoryName,
|
|
81586
|
+
repoPath
|
|
81587
|
+
});
|
|
81588
|
+
return Result.ok({
|
|
81589
|
+
status: "resolved",
|
|
81590
|
+
path: repoPath,
|
|
81591
|
+
repoName: parsedRepoLabel.repositoryName
|
|
81592
|
+
});
|
|
81593
|
+
}
|
|
81594
|
+
const candidates = await getCandidateRepositories(projectsPath);
|
|
81595
|
+
const suggestionsResult = candidates.length ? await linear2.getIssueRepositorySuggestions(issueId, agentSessionId, candidates) : Result.ok([]);
|
|
81596
|
+
const suggestions = Result.isOk(suggestionsResult) ? suggestionsResult.value.toSorted((a, b) => b.confidence - a.confidence).slice(0, 3).map((candidate) => toRepoLabelSuggestion(candidate, candidate.confidence)) : candidates.slice(0, 3).map((candidate) => toRepoLabelSuggestion(candidate, null));
|
|
81597
|
+
log.info("Repo label required before session start", {
|
|
81598
|
+
reason: repoLabel.status,
|
|
81599
|
+
invalidLabel: repoLabel.status === "invalid" ? repoLabel.label : undefined,
|
|
81600
|
+
suggestionCount: suggestions.length
|
|
81601
|
+
});
|
|
81602
|
+
const reason = repoLabel.status === "invalid" ? "invalid" : "missing";
|
|
81603
|
+
return Result.ok({
|
|
81604
|
+
status: "needs_repo_label",
|
|
81605
|
+
reason,
|
|
81606
|
+
invalidLabel: repoLabel.status === "invalid" ? repoLabel.label : undefined,
|
|
81607
|
+
exampleLabel: suggestions[0]?.labelValue ?? "repo:opencode-linear-agent",
|
|
81608
|
+
suggestions
|
|
81609
|
+
});
|
|
81610
|
+
}
|
|
81611
|
+
|
|
81612
|
+
// src/AgentSessionDispatcher.ts
|
|
81613
|
+
function isRecord2(value) {
|
|
81614
|
+
return typeof value === "object" && value !== null;
|
|
81615
|
+
}
|
|
81616
|
+
function readStringField2(value, field) {
|
|
81617
|
+
if (!isRecord2(value)) {
|
|
81618
|
+
return null;
|
|
81619
|
+
}
|
|
81620
|
+
const candidate = value[field];
|
|
81621
|
+
if (typeof candidate !== "string") {
|
|
81622
|
+
return null;
|
|
81623
|
+
}
|
|
81624
|
+
const trimmed = candidate.trim();
|
|
81625
|
+
return trimmed.length > 0 ? candidate : null;
|
|
81626
|
+
}
|
|
81627
|
+
function readPromptContextText2(promptContext) {
|
|
81628
|
+
if (typeof promptContext === "string") {
|
|
81629
|
+
const trimmed = promptContext.trim();
|
|
81630
|
+
return trimmed.length > 0 ? promptContext : null;
|
|
81631
|
+
}
|
|
81632
|
+
const body = readStringField2(promptContext, "body");
|
|
81633
|
+
if (body) {
|
|
81634
|
+
return body;
|
|
81635
|
+
}
|
|
81636
|
+
if (isRecord2(promptContext)) {
|
|
81637
|
+
const contentBody = readStringField2(promptContext.content, "body");
|
|
81638
|
+
if (contentBody) {
|
|
81639
|
+
return contentBody;
|
|
81640
|
+
}
|
|
81641
|
+
}
|
|
81642
|
+
return null;
|
|
81643
|
+
}
|
|
81644
|
+
function extractPromptedUserResponse2(event) {
|
|
81645
|
+
const agentBody = readStringField2(event.agentActivity, "body");
|
|
81646
|
+
if (agentBody) {
|
|
81647
|
+
return agentBody;
|
|
81648
|
+
}
|
|
81649
|
+
if (isRecord2(event.agentActivity)) {
|
|
81650
|
+
const contentBody = readStringField2(event.agentActivity.content, "body");
|
|
81651
|
+
if (contentBody) {
|
|
81652
|
+
return contentBody;
|
|
81653
|
+
}
|
|
81654
|
+
}
|
|
81655
|
+
const promptContextBody = readPromptContextText2(event.promptContext);
|
|
81656
|
+
if (promptContextBody) {
|
|
81657
|
+
return promptContextBody;
|
|
81438
81658
|
}
|
|
81439
|
-
|
|
81440
|
-
|
|
81441
|
-
|
|
81442
|
-
|
|
81659
|
+
return "";
|
|
81660
|
+
}
|
|
81661
|
+
function normalizeMatchInput2(value) {
|
|
81662
|
+
return value.trim().toLowerCase();
|
|
81663
|
+
}
|
|
81664
|
+
function hasWordBoundaryMatch2(haystack, needle) {
|
|
81665
|
+
const regex = new RegExp(`\\b${escapeRegex3(needle)}\\b`, "i");
|
|
81666
|
+
return regex.test(haystack);
|
|
81667
|
+
}
|
|
81668
|
+
function escapeRegex3(str) {
|
|
81669
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
81670
|
+
}
|
|
81671
|
+
function matchRepoSelectionOption(userResponse, options) {
|
|
81672
|
+
const normalizedResponse = normalizeMatchInput2(userResponse);
|
|
81673
|
+
if (normalizedResponse.length === 0) {
|
|
81674
|
+
return null;
|
|
81675
|
+
}
|
|
81676
|
+
for (const opt of options) {
|
|
81677
|
+
if (normalizeMatchInput2(opt.labelValue) === normalizedResponse) {
|
|
81678
|
+
return opt.labelValue;
|
|
81679
|
+
}
|
|
81680
|
+
}
|
|
81681
|
+
for (const opt of options) {
|
|
81682
|
+
for (const alias of opt.aliases) {
|
|
81683
|
+
if (normalizeMatchInput2(alias) === normalizedResponse) {
|
|
81684
|
+
return opt.labelValue;
|
|
81685
|
+
}
|
|
81686
|
+
}
|
|
81687
|
+
}
|
|
81688
|
+
for (const opt of options) {
|
|
81689
|
+
if (normalizedResponse.startsWith(normalizeMatchInput2(opt.labelValue))) {
|
|
81690
|
+
return opt.labelValue;
|
|
81691
|
+
}
|
|
81692
|
+
}
|
|
81693
|
+
for (const opt of options) {
|
|
81694
|
+
for (const alias of opt.aliases) {
|
|
81695
|
+
const normalizedAlias = normalizeMatchInput2(alias);
|
|
81696
|
+
if (normalizedResponse.startsWith(normalizedAlias)) {
|
|
81697
|
+
return opt.labelValue;
|
|
81698
|
+
}
|
|
81699
|
+
}
|
|
81700
|
+
}
|
|
81701
|
+
for (const opt of options) {
|
|
81702
|
+
if (hasWordBoundaryMatch2(userResponse, normalizeMatchInput2(opt.labelValue))) {
|
|
81703
|
+
return opt.labelValue;
|
|
81704
|
+
}
|
|
81705
|
+
}
|
|
81706
|
+
for (const opt of options) {
|
|
81707
|
+
for (const alias of opt.aliases) {
|
|
81708
|
+
const normalizedAlias = normalizeMatchInput2(alias);
|
|
81709
|
+
if (hasWordBoundaryMatch2(userResponse, normalizedAlias)) {
|
|
81710
|
+
return opt.labelValue;
|
|
81711
|
+
}
|
|
81712
|
+
}
|
|
81713
|
+
}
|
|
81714
|
+
return null;
|
|
81715
|
+
}
|
|
81716
|
+
function matchExactRepoSelectionOption(userResponse, options) {
|
|
81717
|
+
const normalizedResponse = normalizeMatchInput2(userResponse);
|
|
81718
|
+
if (normalizedResponse.length === 0) {
|
|
81719
|
+
return null;
|
|
81720
|
+
}
|
|
81721
|
+
for (const opt of options) {
|
|
81722
|
+
if (normalizeMatchInput2(opt.labelValue) === normalizedResponse) {
|
|
81723
|
+
return opt.labelValue;
|
|
81724
|
+
}
|
|
81725
|
+
}
|
|
81726
|
+
for (const opt of options) {
|
|
81727
|
+
for (const alias of opt.aliases) {
|
|
81728
|
+
if (normalizeMatchInput2(alias) === normalizedResponse) {
|
|
81729
|
+
return opt.labelValue;
|
|
81730
|
+
}
|
|
81731
|
+
}
|
|
81732
|
+
}
|
|
81733
|
+
return null;
|
|
81734
|
+
}
|
|
81735
|
+
function buildRepoSelectionOptions(resolution) {
|
|
81736
|
+
return resolution.suggestions.map((suggestion) => {
|
|
81737
|
+
const aliases = [
|
|
81738
|
+
suggestion.labelValue,
|
|
81739
|
+
suggestion.repositoryFullName,
|
|
81740
|
+
suggestion.repositoryName
|
|
81741
|
+
];
|
|
81742
|
+
return {
|
|
81743
|
+
label: suggestion.repositoryFullName,
|
|
81744
|
+
labelValue: suggestion.labelValue,
|
|
81745
|
+
aliases
|
|
81746
|
+
};
|
|
81443
81747
|
});
|
|
81444
|
-
|
|
81748
|
+
}
|
|
81749
|
+
function buildRepoLabelErrorBody(resolution) {
|
|
81750
|
+
const lines = [
|
|
81751
|
+
resolution.reason === "invalid" ? `Missing valid repository label. Replace \`${resolution.invalidLabel ?? "repo:"}\` with a valid \`repo:*\` label before re-running.` : "Missing repository label. Add a `repo:*` label before re-running.",
|
|
81752
|
+
"",
|
|
81753
|
+
`Example: \`${resolution.exampleLabel}\``
|
|
81754
|
+
];
|
|
81755
|
+
if (resolution.suggestions.length > 0) {
|
|
81756
|
+
lines.push("", "Suggested labels:", ...resolution.suggestions.map((suggestion) => suggestion.confidence === null ? `- \`${suggestion.labelValue}\`` : `- \`${suggestion.labelValue}\` (${Math.round(suggestion.confidence * 100)}%)`));
|
|
81757
|
+
}
|
|
81758
|
+
lines.push("", "I stopped before creating any OpenCode session or worktree.");
|
|
81759
|
+
return lines.join(`
|
|
81760
|
+
`);
|
|
81761
|
+
}
|
|
81762
|
+
function buildRepoSelectionBody(resolution, invalidResponse) {
|
|
81763
|
+
const lines = [
|
|
81764
|
+
resolution.reason === "invalid" ? `Replace invalid label \`${resolution.invalidLabel ?? "repo:"}\` by picking a repository or replying with a label like \`${resolution.exampleLabel}\`.` : `Pick a repository or reply with a label like \`${resolution.exampleLabel}\`.`
|
|
81765
|
+
];
|
|
81766
|
+
if (invalidResponse) {
|
|
81767
|
+
lines.push("", `I couldn't use \`${invalidResponse}\`. Reply with \`repo:name\`, \`name\`, or pick one below.`);
|
|
81768
|
+
}
|
|
81769
|
+
return lines.join(`
|
|
81770
|
+
`);
|
|
81771
|
+
}
|
|
81772
|
+
function normalizeRepoLabelInput(userResponse) {
|
|
81773
|
+
const trimmed = userResponse.trim();
|
|
81774
|
+
if (trimmed.length === 0) {
|
|
81775
|
+
return null;
|
|
81776
|
+
}
|
|
81777
|
+
const label = trimmed.startsWith("repo:") ? trimmed : `repo:${trimmed}`;
|
|
81778
|
+
const parsed = parseRepoLabel([{ name: label }]);
|
|
81779
|
+
if (!parsed) {
|
|
81780
|
+
return null;
|
|
81781
|
+
}
|
|
81782
|
+
if (parsed.organizationName) {
|
|
81783
|
+
return `repo:${parsed.organizationName}/${parsed.repositoryName}`;
|
|
81784
|
+
}
|
|
81785
|
+
return `repo:${parsed.repositoryName}`;
|
|
81786
|
+
}
|
|
81787
|
+
function toStartupEvent(event, promptContext) {
|
|
81788
|
+
return {
|
|
81789
|
+
...event,
|
|
81790
|
+
action: "created",
|
|
81791
|
+
promptContext: promptContext ?? event.promptContext
|
|
81792
|
+
};
|
|
81793
|
+
}
|
|
81794
|
+
async function processWithRepo(event, repoPath, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo) {
|
|
81795
|
+
if (processWithResolvedRepo) {
|
|
81796
|
+
await processWithResolvedRepo(event, repoPath);
|
|
81797
|
+
return;
|
|
81798
|
+
}
|
|
81799
|
+
const processor = new LinearEventProcessor(opencode2, linear2, sessionRepository, repoPath, {
|
|
81800
|
+
organizationId: config2.organizationId
|
|
81801
|
+
});
|
|
81802
|
+
await processor.process(event);
|
|
81803
|
+
}
|
|
81804
|
+
async function reportMissingRepoLabel(linear2, linearSessionId, resolution) {
|
|
81805
|
+
await linear2.postError(linearSessionId, new Error(buildRepoLabelErrorBody(resolution)));
|
|
81806
|
+
}
|
|
81807
|
+
async function promptForRepoSelection(linear2, sessionRepository, linearSessionId, issueId, resolution, promptContext, invalidResponse) {
|
|
81808
|
+
const options = buildRepoSelectionOptions(resolution);
|
|
81809
|
+
const pendingSelection = {
|
|
81810
|
+
linearSessionId,
|
|
81811
|
+
issueId,
|
|
81812
|
+
options,
|
|
81813
|
+
promptContext,
|
|
81814
|
+
createdAt: Date.now()
|
|
81815
|
+
};
|
|
81816
|
+
await sessionRepository.savePendingRepoSelection(pendingSelection);
|
|
81817
|
+
await linear2.postElicitation(linearSessionId, buildRepoSelectionBody(resolution, invalidResponse), "select", {
|
|
81818
|
+
options: options.map((option) => ({
|
|
81819
|
+
label: option.label,
|
|
81820
|
+
value: option.labelValue
|
|
81821
|
+
}))
|
|
81822
|
+
});
|
|
81823
|
+
}
|
|
81824
|
+
async function handleRepoSelectionPrompt(event, pendingSelection, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo) {
|
|
81825
|
+
const linearSessionId = event.agentSession.id;
|
|
81826
|
+
const issueId = event.agentSession.issue?.id ?? event.agentSession.issueId;
|
|
81827
|
+
const issueIdentifier = event.agentSession.issue?.identifier ?? issueId ?? "unknown";
|
|
81828
|
+
const log = Log.create({ service: "dispatcher" }).tag("organizationId", config2.organizationId).tag("issue", issueIdentifier);
|
|
81829
|
+
const userResponse = extractPromptedUserResponse2(event);
|
|
81830
|
+
const selectedLabel = matchExactRepoSelectionOption(userResponse, pendingSelection.options) ?? normalizeRepoLabelInput(userResponse) ?? matchRepoSelectionOption(userResponse, pendingSelection.options);
|
|
81831
|
+
if (!selectedLabel) {
|
|
81832
|
+
await promptForRepoSelection(linear2, sessionRepository, linearSessionId, pendingSelection.issueId, {
|
|
81833
|
+
status: "needs_repo_label",
|
|
81834
|
+
reason: "missing",
|
|
81835
|
+
exampleLabel: pendingSelection.options[0]?.labelValue ?? "repo:opencode-linear-agent",
|
|
81836
|
+
suggestions: pendingSelection.options.map((option) => ({
|
|
81837
|
+
confidence: null,
|
|
81838
|
+
hostname: "github.com",
|
|
81839
|
+
labelValue: option.labelValue,
|
|
81840
|
+
repositoryFullName: option.label,
|
|
81841
|
+
repositoryName: option.label.replace(/^.*\//, "")
|
|
81842
|
+
}))
|
|
81843
|
+
}, pendingSelection.promptContext, userResponse);
|
|
81844
|
+
return;
|
|
81845
|
+
}
|
|
81846
|
+
const setLabelResult = await linear2.setIssueRepoLabel(pendingSelection.issueId, selectedLabel);
|
|
81847
|
+
if (Result.isError(setLabelResult)) {
|
|
81848
|
+
log.error("Failed to set selected repo label", {
|
|
81849
|
+
labelValue: selectedLabel,
|
|
81850
|
+
error: setLabelResult.error.message,
|
|
81851
|
+
errorType: setLabelResult.error._tag
|
|
81852
|
+
});
|
|
81853
|
+
await linear2.postError(linearSessionId, setLabelResult.error);
|
|
81854
|
+
return;
|
|
81855
|
+
}
|
|
81856
|
+
await sessionRepository.deletePendingRepoSelection(linearSessionId);
|
|
81857
|
+
const parsed = findRepoLabel([{ name: selectedLabel }]);
|
|
81858
|
+
if (parsed.status !== "valid") {
|
|
81859
|
+
await linear2.postError(linearSessionId, new Error(`Invalid repo label selected: ${selectedLabel}`));
|
|
81860
|
+
return;
|
|
81861
|
+
}
|
|
81862
|
+
await processWithRepo(toStartupEvent(event, pendingSelection.promptContext), `${config2.projectsPath}/${parsed.value.repositoryName}`, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo);
|
|
81863
|
+
}
|
|
81864
|
+
async function dispatchAgentSessionEvent(event, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo) {
|
|
81865
|
+
const linearSessionId = event.agentSession.id;
|
|
81866
|
+
const issueId = event.agentSession.issue?.id ?? event.agentSession.issueId;
|
|
81867
|
+
const issueIdentifier = event.agentSession.issue?.identifier ?? issueId ?? "unknown";
|
|
81868
|
+
const log = Log.create({ service: "dispatcher" }).tag("organizationId", config2.organizationId).tag("issue", issueIdentifier);
|
|
81869
|
+
const pendingSelection = await sessionRepository.getPendingRepoSelection(linearSessionId);
|
|
81870
|
+
if (pendingSelection) {
|
|
81871
|
+
await handleRepoSelectionPrompt(event, pendingSelection, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo);
|
|
81872
|
+
return;
|
|
81873
|
+
}
|
|
81874
|
+
const sessionState = await sessionRepository.get(linearSessionId);
|
|
81875
|
+
if (event.action === "prompted" && sessionState?.repoDirectory) {
|
|
81876
|
+
log.info("Using existing session repo directory", {
|
|
81877
|
+
repoPath: sessionState.repoDirectory
|
|
81878
|
+
});
|
|
81879
|
+
await processWithRepo(event, sessionState.repoDirectory, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo);
|
|
81880
|
+
return;
|
|
81881
|
+
}
|
|
81882
|
+
if (!issueId) {
|
|
81883
|
+
await linear2.postError(linearSessionId, new Error("Missing issue id"));
|
|
81884
|
+
return;
|
|
81885
|
+
}
|
|
81886
|
+
const resolveResult = await resolveRepoPath(linear2, issueId, linearSessionId, config2.projectsPath);
|
|
81887
|
+
if (Result.isError(resolveResult)) {
|
|
81888
|
+
log.error("Failed to resolve repository", {
|
|
81889
|
+
error: resolveResult.error.message
|
|
81890
|
+
});
|
|
81891
|
+
await linear2.postError(linearSessionId, resolveResult.error);
|
|
81892
|
+
return;
|
|
81893
|
+
}
|
|
81894
|
+
const resolved = resolveResult.value;
|
|
81895
|
+
if (resolved.status === "needs_repo_label") {
|
|
81896
|
+
if (resolved.suggestions.length > 0) {
|
|
81897
|
+
await promptForRepoSelection(linear2, sessionRepository, linearSessionId, issueId, resolved, readPromptContextText2(event.promptContext) ?? undefined);
|
|
81898
|
+
return;
|
|
81899
|
+
}
|
|
81900
|
+
await reportMissingRepoLabel(linear2, linearSessionId, resolved);
|
|
81901
|
+
return;
|
|
81902
|
+
}
|
|
81903
|
+
log.info("Using repository path", {
|
|
81904
|
+
repoPath: resolved.path,
|
|
81905
|
+
repoName: resolved.repoName
|
|
81906
|
+
});
|
|
81907
|
+
await processWithRepo(sessionState ? event : toStartupEvent(event), resolved.path, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo);
|
|
81445
81908
|
}
|
|
81446
81909
|
|
|
81447
81910
|
// src/index.ts
|
|
@@ -81487,13 +81950,9 @@ function createDirectDispatcher(config2, tokenStore, sessionRepository) {
|
|
|
81487
81950
|
await issueHandler.process(event);
|
|
81488
81951
|
return;
|
|
81489
81952
|
}
|
|
81490
|
-
const linearSessionId = event.agentSession.id;
|
|
81491
|
-
const issueId = event.agentSession.issue?.id ?? event.agentSession.issueId ?? "unknown";
|
|
81492
|
-
const issueIdentifier = event.agentSession.issue?.identifier ?? issueId;
|
|
81493
|
-
const log = Log.create({ service: "dispatcher" }).tag("organizationId", organizationId).tag("issue", issueIdentifier);
|
|
81494
81953
|
let accessToken = await tokenStore.getAccessToken(organizationId);
|
|
81495
81954
|
if (!accessToken) {
|
|
81496
|
-
|
|
81955
|
+
Log.create({ service: "dispatcher" }).tag("organizationId", organizationId).info("No access token, attempting refresh");
|
|
81497
81956
|
const oauthConfig = {
|
|
81498
81957
|
clientId: config2.linear.clientId,
|
|
81499
81958
|
clientSecret: config2.linear.clientSecret
|
|
@@ -81501,23 +81960,10 @@ function createDirectDispatcher(config2, tokenStore, sessionRepository) {
|
|
|
81501
81960
|
accessToken = await refreshAccessToken(oauthConfig, tokenStore, organizationId);
|
|
81502
81961
|
}
|
|
81503
81962
|
const linear2 = new LinearServiceImpl(accessToken);
|
|
81504
|
-
|
|
81505
|
-
|
|
81506
|
-
|
|
81507
|
-
error: resolveResult.error.message
|
|
81508
|
-
});
|
|
81509
|
-
await linear2.postError(linearSessionId, resolveResult.error);
|
|
81510
|
-
return;
|
|
81511
|
-
}
|
|
81512
|
-
const resolved = resolveResult.value;
|
|
81513
|
-
log.info("Using repository path", {
|
|
81514
|
-
repoPath: resolved.path,
|
|
81515
|
-
repoName: resolved.repoName
|
|
81963
|
+
await dispatchAgentSessionEvent(event, linear2, opencode2, sessionRepository, {
|
|
81964
|
+
organizationId,
|
|
81965
|
+
projectsPath: config2.projectsPath
|
|
81516
81966
|
});
|
|
81517
|
-
const processor = new LinearEventProcessor(opencode2, linear2, sessionRepository, resolved.path, {
|
|
81518
|
-
organizationId
|
|
81519
|
-
});
|
|
81520
|
-
await processor.process(event);
|
|
81521
81967
|
}
|
|
81522
81968
|
};
|
|
81523
81969
|
}
|
|
@@ -81651,11 +82097,13 @@ Make sure OpenCode is running: opencode serve
|
|
|
81651
82097
|
`);
|
|
81652
82098
|
return server2;
|
|
81653
82099
|
}
|
|
81654
|
-
|
|
81655
|
-
|
|
81656
|
-
|
|
81657
|
-
error
|
|
81658
|
-
|
|
82100
|
+
if (import.meta.main) {
|
|
82101
|
+
main().catch((error48) => {
|
|
82102
|
+
const log = Log.create({ service: "startup" });
|
|
82103
|
+
log.error("Failed to start server", {
|
|
82104
|
+
error: error48 instanceof Error ? error48.message : String(error48),
|
|
82105
|
+
stack: error48 instanceof Error ? error48.stack : undefined
|
|
82106
|
+
});
|
|
82107
|
+
process.exit(1);
|
|
81659
82108
|
});
|
|
81660
|
-
|
|
81661
|
-
});
|
|
82109
|
+
}
|