@opencode-linear-agent/server 0.1.3-master.11 → 0.1.3-master.14

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.
Files changed (2) hide show
  1. package/dist/index.js +514 -49
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3876,7 +3876,10 @@ class WorktreeManager {
3876
3876
  });
3877
3877
  }
3878
3878
  buildWorktreeName(issue, linearSessionId) {
3879
- const safeIssue = issue.toLowerCase().replace(/[^a-z0-9-]/g, "-");
3879
+ if (issue.branchName) {
3880
+ return `${linearSessionId}/${issue.branchName}`;
3881
+ }
3882
+ const safeIssue = issue.identifier.toLowerCase().replace(/[^a-z0-9-]/g, "-");
3880
3883
  const sessionSuffix = linearSessionId.slice(0, 8).toLowerCase();
3881
3884
  return `${safeIssue}-${sessionSuffix}`;
3882
3885
  }
@@ -4229,8 +4232,21 @@ class LinearEventProcessor {
4229
4232
  async process(event) {
4230
4233
  const linearSessionId = event.agentSession.id;
4231
4234
  const issueId = event.agentSession.issue?.id ?? event.agentSession.issueId;
4232
- const issueIdentifier = event.agentSession.issue?.identifier ?? issueId ?? "unknown";
4233
- const log = Log.create({ service: "processor" }).tag("issue", issueIdentifier).tag("sessionId", linearSessionId);
4235
+ const fallbackIssueIdentifier = event.agentSession.issue?.identifier ?? issueId ?? "unknown";
4236
+ let issue = {
4237
+ identifier: fallbackIssueIdentifier,
4238
+ branchName: readStringField(event.agentSession.issue, "branchName") ?? undefined
4239
+ };
4240
+ if (issueId && !issue.branchName) {
4241
+ const issueResult = await this.linear.getIssue(issueId);
4242
+ if (Result.isOk(issueResult)) {
4243
+ issue = {
4244
+ identifier: issueResult.value.identifier,
4245
+ branchName: issueResult.value.branchName
4246
+ };
4247
+ }
4248
+ }
4249
+ const log = Log.create({ service: "processor" }).tag("issue", issue.identifier).tag("sessionId", linearSessionId);
4234
4250
  log.info("Processing event", { action: event.action });
4235
4251
  const action = this.toSessionWorktreeAction(event.action);
4236
4252
  if (!action) {
@@ -4239,7 +4255,7 @@ class LinearEventProcessor {
4239
4255
  });
4240
4256
  return;
4241
4257
  }
4242
- const worktreeResult = await this.worktreeManager.resolveWorktree(linearSessionId, issueIdentifier, action, log);
4258
+ const worktreeResult = await this.worktreeManager.resolveWorktree(linearSessionId, issue, action, log);
4243
4259
  if (Result.isError(worktreeResult)) {
4244
4260
  await this.linear.postError(linearSessionId, worktreeResult.error);
4245
4261
  return;
@@ -66849,6 +66865,16 @@ function getOpencodeErrorMessage(error) {
66849
66865
  return JSON.stringify(error);
66850
66866
  }
66851
66867
  // ../core/src/linear/LinearServiceImpl.ts
66868
+ async function collectTeamLabels(team) {
66869
+ const first = await team.labels();
66870
+ const labels = [...first.nodes];
66871
+ let page = first;
66872
+ while (page.pageInfo.hasNextPage) {
66873
+ page = await page.fetchNext();
66874
+ labels.push(...page.nodes);
66875
+ }
66876
+ return labels;
66877
+ }
66852
66878
  async function collectIssueAgentSessionIds(fetchPage) {
66853
66879
  const ids = new Set;
66854
66880
  let after;
@@ -67043,6 +67069,7 @@ ${truncatedStack}
67043
67069
  return {
67044
67070
  id: issue.id,
67045
67071
  identifier: issue.identifier,
67072
+ branchName: issue.branchName ?? undefined,
67046
67073
  title: issue.title,
67047
67074
  description: issue.description ?? undefined,
67048
67075
  url: issue.url
@@ -67102,6 +67129,92 @@ ${truncatedStack}
67102
67129
  }
67103
67130
  return result;
67104
67131
  }
67132
+ async getIssueRepositorySuggestions(issueId, agentSessionId, candidates) {
67133
+ const result = await Result.tryPromise({
67134
+ try: async () => {
67135
+ const response = await this.client.client.rawRequest(`query IssueRepositorySuggestions($issueId: String!, $agentSessionId: String!, $candidateRepositories: [RepositoryInput!]!) {
67136
+ issueRepositorySuggestions(
67137
+ issueId: $issueId
67138
+ agentSessionId: $agentSessionId
67139
+ candidateRepositories: $candidateRepositories
67140
+ ) {
67141
+ suggestions {
67142
+ repositoryFullName
67143
+ hostname
67144
+ confidence
67145
+ }
67146
+ }
67147
+ }`, {
67148
+ issueId,
67149
+ agentSessionId,
67150
+ candidateRepositories: candidates
67151
+ });
67152
+ if (!response.data) {
67153
+ return [];
67154
+ }
67155
+ return response.data.issueRepositorySuggestions.suggestions;
67156
+ },
67157
+ catch: mapLinearError
67158
+ });
67159
+ if (Result.isError(result)) {
67160
+ this.log.error("Failed to get issue repository suggestions", {
67161
+ issueId,
67162
+ agentSessionId,
67163
+ candidateCount: candidates.length,
67164
+ error: result.error.message,
67165
+ errorType: result.error._tag
67166
+ });
67167
+ }
67168
+ return result;
67169
+ }
67170
+ async setIssueRepoLabel(issueId, labelName) {
67171
+ const result = await Result.tryPromise({
67172
+ try: async () => {
67173
+ const issue = await this.client.issue(issueId);
67174
+ const team = await issue.team;
67175
+ if (!team) {
67176
+ throw new Error("Issue has no associated team");
67177
+ }
67178
+ const issueLabels = await issue.labels();
67179
+ const teamLabels = await collectTeamLabels(team);
67180
+ let repoLabelId = teamLabels.find((label) => label.name.toLowerCase() === labelName.toLowerCase())?.id;
67181
+ if (!repoLabelId) {
67182
+ const response = await this.client.client.rawRequest(`mutation IssueLabelCreate($input: IssueLabelCreateInput!) {
67183
+ issueLabelCreate(input: $input) {
67184
+ success
67185
+ issueLabel {
67186
+ id
67187
+ }
67188
+ }
67189
+ }`, {
67190
+ input: {
67191
+ name: labelName,
67192
+ teamId: team.id
67193
+ }
67194
+ });
67195
+ repoLabelId = response.data?.issueLabelCreate.issueLabel?.id;
67196
+ if (!repoLabelId) {
67197
+ throw new Error(`Failed to create repo label ${labelName}`);
67198
+ }
67199
+ }
67200
+ const labelIds = issueLabels.nodes.filter((label) => !label.name.startsWith("repo:")).map((label) => label.id);
67201
+ await issue.update({
67202
+ labelIds: [...labelIds, repoLabelId]
67203
+ });
67204
+ },
67205
+ catch: mapLinearError
67206
+ });
67207
+ if (Result.isError(result)) {
67208
+ this.log.error("Failed to set repo label on issue", {
67209
+ issueId,
67210
+ labelName,
67211
+ error: result.error.message,
67212
+ errorType: result.error._tag
67213
+ });
67214
+ return Result.err(result.error);
67215
+ }
67216
+ return Result.ok(undefined);
67217
+ }
67105
67218
  async getIssueAgentSessionIds(issueId) {
67106
67219
  const result = await Result.tryPromise({
67107
67220
  try: async () => {
@@ -67181,29 +67294,44 @@ ${truncatedStack}
67181
67294
  }
67182
67295
  }
67183
67296
  // ../core/src/linear/label-parser.ts
67184
- function parseRepoLabel(labels) {
67297
+ function findRepoLabel(labels) {
67185
67298
  const repoLabel = labels.find((label) => label.name.startsWith("repo:"));
67186
67299
  if (!repoLabel) {
67187
- return null;
67300
+ return { status: "missing" };
67188
67301
  }
67189
67302
  const repoPath = repoLabel.name.slice(5);
67190
67303
  if (!repoPath.trim()) {
67191
- return null;
67304
+ return { status: "invalid", label: repoLabel.name };
67192
67305
  }
67193
67306
  if (repoPath.includes("/")) {
67194
67307
  const [organizationName, repositoryName] = repoPath.split("/", 2);
67195
67308
  if (!organizationName?.trim() || !repositoryName?.trim()) {
67196
- return null;
67309
+ return { status: "invalid", label: repoLabel.name };
67197
67310
  }
67198
67311
  return {
67199
- organizationName: organizationName.trim(),
67200
- repositoryName: repositoryName.trim()
67312
+ status: "valid",
67313
+ label: repoLabel.name,
67314
+ value: {
67315
+ organizationName: organizationName.trim(),
67316
+ repositoryName: repositoryName.trim()
67317
+ }
67201
67318
  };
67202
67319
  }
67203
67320
  return {
67204
- repositoryName: repoPath.trim()
67321
+ status: "valid",
67322
+ label: repoLabel.name,
67323
+ value: {
67324
+ repositoryName: repoPath.trim()
67325
+ }
67205
67326
  };
67206
67327
  }
67328
+ function parseRepoLabel(labels) {
67329
+ const result = findRepoLabel(labels);
67330
+ if (result.status !== "valid") {
67331
+ return null;
67332
+ }
67333
+ return result.value;
67334
+ }
67207
67335
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
67208
67336
  var exports_external = {};
67209
67337
  __export(exports_external, {
@@ -81389,6 +81517,7 @@ class FileTokenStore {
81389
81517
  var SESSION_PREFIX = "session:";
81390
81518
  var QUESTION_PREFIX = "question:";
81391
81519
  var PERMISSION_PREFIX = "permission:";
81520
+ var REPO_SELECTION_PREFIX = "repo-selection:";
81392
81521
 
81393
81522
  class FileSessionRepository {
81394
81523
  kv;
@@ -81422,26 +81551,377 @@ class FileSessionRepository {
81422
81551
  async deletePendingPermission(linearSessionId) {
81423
81552
  await this.kv.delete(`${PERMISSION_PREFIX}${linearSessionId}`);
81424
81553
  }
81554
+ async getPendingRepoSelection(linearSessionId) {
81555
+ return this.kv.get(`${REPO_SELECTION_PREFIX}${linearSessionId}`);
81556
+ }
81557
+ async savePendingRepoSelection(selection) {
81558
+ await this.kv.put(`${REPO_SELECTION_PREFIX}${selection.linearSessionId}`, selection);
81559
+ }
81560
+ async deletePendingRepoSelection(linearSessionId) {
81561
+ await this.kv.delete(`${REPO_SELECTION_PREFIX}${linearSessionId}`);
81562
+ }
81425
81563
  }
81426
81564
  // src/RepoResolver.ts
81427
81565
  import { join as join3 } from "path";
81428
- async function resolveRepoPath(linear2, issueId, projectsPath) {
81566
+ import { readdir } from "fs/promises";
81567
+ function toRepoLabelSuggestion(candidate, confidence) {
81568
+ const parts = candidate.repositoryFullName.split("/");
81569
+ const repositoryName = parts[parts.length - 1] ?? candidate.repositoryFullName;
81570
+ return {
81571
+ confidence,
81572
+ hostname: candidate.hostname,
81573
+ labelValue: `repo:${candidate.repositoryFullName}`,
81574
+ repositoryFullName: candidate.repositoryFullName,
81575
+ repositoryName
81576
+ };
81577
+ }
81578
+ async function getCandidateRepositories(projectsPath) {
81579
+ const result = await Result.tryPromise({
81580
+ try: async () => readdir(projectsPath, { withFileTypes: true }),
81581
+ catch: (e) => e instanceof Error ? e.message : String(e)
81582
+ });
81583
+ if (Result.isError(result)) {
81584
+ return [];
81585
+ }
81586
+ return result.value.filter((entry) => entry.isDirectory()).map((entry) => ({
81587
+ hostname: "github.com",
81588
+ repositoryFullName: entry.name
81589
+ }));
81590
+ }
81591
+ async function resolveRepoPath(linear2, issueId, agentSessionId, projectsPath) {
81429
81592
  const log = Log.create({ service: "repo-resolver" }).tag("issueId", issueId);
81430
81593
  const labelsResult = await linear2.getIssueLabels(issueId);
81431
81594
  if (Result.isError(labelsResult)) {
81432
81595
  return Result.err(labelsResult.error);
81433
81596
  }
81434
- const repoLabel = parseRepoLabel(labelsResult.value);
81435
- if (!repoLabel) {
81436
- log.info("No repo label, using projectsPath root", { projectsPath });
81437
- return Result.ok({ path: projectsPath, repoName: null });
81597
+ const parsedRepoLabel = parseRepoLabel(labelsResult.value);
81598
+ const repoLabel = findRepoLabel(labelsResult.value);
81599
+ if (parsedRepoLabel) {
81600
+ const repoPath = join3(projectsPath, parsedRepoLabel.repositoryName);
81601
+ log.info("Resolved repo from label", {
81602
+ repoName: parsedRepoLabel.repositoryName,
81603
+ repoPath
81604
+ });
81605
+ return Result.ok({
81606
+ status: "resolved",
81607
+ path: repoPath,
81608
+ repoName: parsedRepoLabel.repositoryName
81609
+ });
81438
81610
  }
81439
- const repoPath = join3(projectsPath, repoLabel.repositoryName);
81440
- log.info("Resolved repo from label", {
81441
- repoName: repoLabel.repositoryName,
81442
- repoPath
81611
+ const candidates = await getCandidateRepositories(projectsPath);
81612
+ const suggestionsResult = candidates.length ? await linear2.getIssueRepositorySuggestions(issueId, agentSessionId, candidates) : Result.ok([]);
81613
+ 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));
81614
+ log.info("Repo label required before session start", {
81615
+ reason: repoLabel.status,
81616
+ invalidLabel: repoLabel.status === "invalid" ? repoLabel.label : undefined,
81617
+ suggestionCount: suggestions.length
81618
+ });
81619
+ const reason = repoLabel.status === "invalid" ? "invalid" : "missing";
81620
+ return Result.ok({
81621
+ status: "needs_repo_label",
81622
+ reason,
81623
+ invalidLabel: repoLabel.status === "invalid" ? repoLabel.label : undefined,
81624
+ exampleLabel: suggestions[0]?.labelValue ?? "repo:opencode-linear-agent",
81625
+ suggestions
81443
81626
  });
81444
- return Result.ok({ path: repoPath, repoName: repoLabel.repositoryName });
81627
+ }
81628
+
81629
+ // src/AgentSessionDispatcher.ts
81630
+ function isRecord2(value) {
81631
+ return typeof value === "object" && value !== null;
81632
+ }
81633
+ function readStringField2(value, field) {
81634
+ if (!isRecord2(value)) {
81635
+ return null;
81636
+ }
81637
+ const candidate = value[field];
81638
+ if (typeof candidate !== "string") {
81639
+ return null;
81640
+ }
81641
+ const trimmed = candidate.trim();
81642
+ return trimmed.length > 0 ? candidate : null;
81643
+ }
81644
+ function readPromptContextText2(promptContext) {
81645
+ if (typeof promptContext === "string") {
81646
+ const trimmed = promptContext.trim();
81647
+ return trimmed.length > 0 ? promptContext : null;
81648
+ }
81649
+ const body = readStringField2(promptContext, "body");
81650
+ if (body) {
81651
+ return body;
81652
+ }
81653
+ if (isRecord2(promptContext)) {
81654
+ const contentBody = readStringField2(promptContext.content, "body");
81655
+ if (contentBody) {
81656
+ return contentBody;
81657
+ }
81658
+ }
81659
+ return null;
81660
+ }
81661
+ function extractPromptedUserResponse2(event) {
81662
+ const agentBody = readStringField2(event.agentActivity, "body");
81663
+ if (agentBody) {
81664
+ return agentBody;
81665
+ }
81666
+ if (isRecord2(event.agentActivity)) {
81667
+ const contentBody = readStringField2(event.agentActivity.content, "body");
81668
+ if (contentBody) {
81669
+ return contentBody;
81670
+ }
81671
+ }
81672
+ const promptContextBody = readPromptContextText2(event.promptContext);
81673
+ if (promptContextBody) {
81674
+ return promptContextBody;
81675
+ }
81676
+ return "";
81677
+ }
81678
+ function normalizeMatchInput2(value) {
81679
+ return value.trim().toLowerCase();
81680
+ }
81681
+ function hasWordBoundaryMatch2(haystack, needle) {
81682
+ const regex = new RegExp(`\\b${escapeRegex3(needle)}\\b`, "i");
81683
+ return regex.test(haystack);
81684
+ }
81685
+ function escapeRegex3(str) {
81686
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
81687
+ }
81688
+ function matchRepoSelectionOption(userResponse, options) {
81689
+ const normalizedResponse = normalizeMatchInput2(userResponse);
81690
+ if (normalizedResponse.length === 0) {
81691
+ return null;
81692
+ }
81693
+ for (const opt of options) {
81694
+ if (normalizeMatchInput2(opt.labelValue) === normalizedResponse) {
81695
+ return opt.labelValue;
81696
+ }
81697
+ }
81698
+ for (const opt of options) {
81699
+ for (const alias of opt.aliases) {
81700
+ if (normalizeMatchInput2(alias) === normalizedResponse) {
81701
+ return opt.labelValue;
81702
+ }
81703
+ }
81704
+ }
81705
+ for (const opt of options) {
81706
+ if (normalizedResponse.startsWith(normalizeMatchInput2(opt.labelValue))) {
81707
+ return opt.labelValue;
81708
+ }
81709
+ }
81710
+ for (const opt of options) {
81711
+ for (const alias of opt.aliases) {
81712
+ const normalizedAlias = normalizeMatchInput2(alias);
81713
+ if (normalizedResponse.startsWith(normalizedAlias)) {
81714
+ return opt.labelValue;
81715
+ }
81716
+ }
81717
+ }
81718
+ for (const opt of options) {
81719
+ if (hasWordBoundaryMatch2(userResponse, normalizeMatchInput2(opt.labelValue))) {
81720
+ return opt.labelValue;
81721
+ }
81722
+ }
81723
+ for (const opt of options) {
81724
+ for (const alias of opt.aliases) {
81725
+ const normalizedAlias = normalizeMatchInput2(alias);
81726
+ if (hasWordBoundaryMatch2(userResponse, normalizedAlias)) {
81727
+ return opt.labelValue;
81728
+ }
81729
+ }
81730
+ }
81731
+ return null;
81732
+ }
81733
+ function matchExactRepoSelectionOption(userResponse, options) {
81734
+ const normalizedResponse = normalizeMatchInput2(userResponse);
81735
+ if (normalizedResponse.length === 0) {
81736
+ return null;
81737
+ }
81738
+ for (const opt of options) {
81739
+ if (normalizeMatchInput2(opt.labelValue) === normalizedResponse) {
81740
+ return opt.labelValue;
81741
+ }
81742
+ }
81743
+ for (const opt of options) {
81744
+ for (const alias of opt.aliases) {
81745
+ if (normalizeMatchInput2(alias) === normalizedResponse) {
81746
+ return opt.labelValue;
81747
+ }
81748
+ }
81749
+ }
81750
+ return null;
81751
+ }
81752
+ function buildRepoSelectionOptions(resolution) {
81753
+ return resolution.suggestions.map((suggestion) => {
81754
+ const aliases = [
81755
+ suggestion.labelValue,
81756
+ suggestion.repositoryFullName,
81757
+ suggestion.repositoryName
81758
+ ];
81759
+ return {
81760
+ label: suggestion.repositoryFullName,
81761
+ labelValue: suggestion.labelValue,
81762
+ aliases
81763
+ };
81764
+ });
81765
+ }
81766
+ function buildRepoLabelErrorBody(resolution) {
81767
+ const lines = [
81768
+ 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.",
81769
+ "",
81770
+ `Example: \`${resolution.exampleLabel}\``
81771
+ ];
81772
+ if (resolution.suggestions.length > 0) {
81773
+ lines.push("", "Suggested labels:", ...resolution.suggestions.map((suggestion) => suggestion.confidence === null ? `- \`${suggestion.labelValue}\`` : `- \`${suggestion.labelValue}\` (${Math.round(suggestion.confidence * 100)}%)`));
81774
+ }
81775
+ lines.push("", "I stopped before creating any OpenCode session or worktree.");
81776
+ return lines.join(`
81777
+ `);
81778
+ }
81779
+ function buildRepoSelectionBody(resolution, invalidResponse) {
81780
+ const lines = [
81781
+ 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}\`.`
81782
+ ];
81783
+ if (invalidResponse) {
81784
+ lines.push("", `I couldn't use \`${invalidResponse}\`. Reply with \`repo:name\`, \`name\`, or pick one below.`);
81785
+ }
81786
+ return lines.join(`
81787
+ `);
81788
+ }
81789
+ function normalizeRepoLabelInput(userResponse) {
81790
+ const trimmed = userResponse.trim();
81791
+ if (trimmed.length === 0) {
81792
+ return null;
81793
+ }
81794
+ const label = trimmed.startsWith("repo:") ? trimmed : `repo:${trimmed}`;
81795
+ const parsed = parseRepoLabel([{ name: label }]);
81796
+ if (!parsed) {
81797
+ return null;
81798
+ }
81799
+ if (parsed.organizationName) {
81800
+ return `repo:${parsed.organizationName}/${parsed.repositoryName}`;
81801
+ }
81802
+ return `repo:${parsed.repositoryName}`;
81803
+ }
81804
+ function toStartupEvent(event, promptContext) {
81805
+ return {
81806
+ ...event,
81807
+ action: "created",
81808
+ promptContext: promptContext ?? event.promptContext
81809
+ };
81810
+ }
81811
+ async function processWithRepo(event, repoPath, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo) {
81812
+ if (processWithResolvedRepo) {
81813
+ await processWithResolvedRepo(event, repoPath);
81814
+ return;
81815
+ }
81816
+ const processor = new LinearEventProcessor(opencode2, linear2, sessionRepository, repoPath, {
81817
+ organizationId: config2.organizationId
81818
+ });
81819
+ await processor.process(event);
81820
+ }
81821
+ async function reportMissingRepoLabel(linear2, linearSessionId, resolution) {
81822
+ await linear2.postError(linearSessionId, new Error(buildRepoLabelErrorBody(resolution)));
81823
+ }
81824
+ async function promptForRepoSelection(linear2, sessionRepository, linearSessionId, issueId, resolution, promptContext, invalidResponse) {
81825
+ const options = buildRepoSelectionOptions(resolution);
81826
+ const pendingSelection = {
81827
+ linearSessionId,
81828
+ issueId,
81829
+ options,
81830
+ promptContext,
81831
+ createdAt: Date.now()
81832
+ };
81833
+ await sessionRepository.savePendingRepoSelection(pendingSelection);
81834
+ await linear2.postElicitation(linearSessionId, buildRepoSelectionBody(resolution, invalidResponse), "select", {
81835
+ options: options.map((option) => ({
81836
+ label: option.label,
81837
+ value: option.labelValue
81838
+ }))
81839
+ });
81840
+ }
81841
+ async function handleRepoSelectionPrompt(event, pendingSelection, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo) {
81842
+ const linearSessionId = event.agentSession.id;
81843
+ const issueId = event.agentSession.issue?.id ?? event.agentSession.issueId;
81844
+ const issueIdentifier = event.agentSession.issue?.identifier ?? issueId ?? "unknown";
81845
+ const log = Log.create({ service: "dispatcher" }).tag("organizationId", config2.organizationId).tag("issue", issueIdentifier);
81846
+ const userResponse = extractPromptedUserResponse2(event);
81847
+ const selectedLabel = matchExactRepoSelectionOption(userResponse, pendingSelection.options) ?? normalizeRepoLabelInput(userResponse) ?? matchRepoSelectionOption(userResponse, pendingSelection.options);
81848
+ if (!selectedLabel) {
81849
+ await promptForRepoSelection(linear2, sessionRepository, linearSessionId, pendingSelection.issueId, {
81850
+ status: "needs_repo_label",
81851
+ reason: "missing",
81852
+ exampleLabel: pendingSelection.options[0]?.labelValue ?? "repo:opencode-linear-agent",
81853
+ suggestions: pendingSelection.options.map((option) => ({
81854
+ confidence: null,
81855
+ hostname: "github.com",
81856
+ labelValue: option.labelValue,
81857
+ repositoryFullName: option.label,
81858
+ repositoryName: option.label.replace(/^.*\//, "")
81859
+ }))
81860
+ }, pendingSelection.promptContext, userResponse);
81861
+ return;
81862
+ }
81863
+ const setLabelResult = await linear2.setIssueRepoLabel(pendingSelection.issueId, selectedLabel);
81864
+ if (Result.isError(setLabelResult)) {
81865
+ log.error("Failed to set selected repo label", {
81866
+ labelValue: selectedLabel,
81867
+ error: setLabelResult.error.message,
81868
+ errorType: setLabelResult.error._tag
81869
+ });
81870
+ await linear2.postError(linearSessionId, setLabelResult.error);
81871
+ return;
81872
+ }
81873
+ await sessionRepository.deletePendingRepoSelection(linearSessionId);
81874
+ const parsed = findRepoLabel([{ name: selectedLabel }]);
81875
+ if (parsed.status !== "valid") {
81876
+ await linear2.postError(linearSessionId, new Error(`Invalid repo label selected: ${selectedLabel}`));
81877
+ return;
81878
+ }
81879
+ await processWithRepo(toStartupEvent(event, pendingSelection.promptContext), `${config2.projectsPath}/${parsed.value.repositoryName}`, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo);
81880
+ }
81881
+ async function dispatchAgentSessionEvent(event, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo) {
81882
+ const linearSessionId = event.agentSession.id;
81883
+ const issueId = event.agentSession.issue?.id ?? event.agentSession.issueId;
81884
+ const issueIdentifier = event.agentSession.issue?.identifier ?? issueId ?? "unknown";
81885
+ const log = Log.create({ service: "dispatcher" }).tag("organizationId", config2.organizationId).tag("issue", issueIdentifier);
81886
+ const pendingSelection = await sessionRepository.getPendingRepoSelection(linearSessionId);
81887
+ if (pendingSelection) {
81888
+ await handleRepoSelectionPrompt(event, pendingSelection, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo);
81889
+ return;
81890
+ }
81891
+ const sessionState = await sessionRepository.get(linearSessionId);
81892
+ if (event.action === "prompted" && sessionState?.repoDirectory) {
81893
+ log.info("Using existing session repo directory", {
81894
+ repoPath: sessionState.repoDirectory
81895
+ });
81896
+ await processWithRepo(event, sessionState.repoDirectory, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo);
81897
+ return;
81898
+ }
81899
+ if (!issueId) {
81900
+ await linear2.postError(linearSessionId, new Error("Missing issue id"));
81901
+ return;
81902
+ }
81903
+ const resolveResult = await resolveRepoPath(linear2, issueId, linearSessionId, config2.projectsPath);
81904
+ if (Result.isError(resolveResult)) {
81905
+ log.error("Failed to resolve repository", {
81906
+ error: resolveResult.error.message
81907
+ });
81908
+ await linear2.postError(linearSessionId, resolveResult.error);
81909
+ return;
81910
+ }
81911
+ const resolved = resolveResult.value;
81912
+ if (resolved.status === "needs_repo_label") {
81913
+ if (resolved.suggestions.length > 0) {
81914
+ await promptForRepoSelection(linear2, sessionRepository, linearSessionId, issueId, resolved, readPromptContextText2(event.promptContext) ?? undefined);
81915
+ return;
81916
+ }
81917
+ await reportMissingRepoLabel(linear2, linearSessionId, resolved);
81918
+ return;
81919
+ }
81920
+ log.info("Using repository path", {
81921
+ repoPath: resolved.path,
81922
+ repoName: resolved.repoName
81923
+ });
81924
+ await processWithRepo(sessionState ? event : toStartupEvent(event), resolved.path, linear2, opencode2, sessionRepository, config2, processWithResolvedRepo);
81445
81925
  }
81446
81926
 
81447
81927
  // src/index.ts
@@ -81487,13 +81967,9 @@ function createDirectDispatcher(config2, tokenStore, sessionRepository) {
81487
81967
  await issueHandler.process(event);
81488
81968
  return;
81489
81969
  }
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
81970
  let accessToken = await tokenStore.getAccessToken(organizationId);
81495
81971
  if (!accessToken) {
81496
- log.info("No access token, attempting refresh");
81972
+ Log.create({ service: "dispatcher" }).tag("organizationId", organizationId).info("No access token, attempting refresh");
81497
81973
  const oauthConfig = {
81498
81974
  clientId: config2.linear.clientId,
81499
81975
  clientSecret: config2.linear.clientSecret
@@ -81501,23 +81977,10 @@ function createDirectDispatcher(config2, tokenStore, sessionRepository) {
81501
81977
  accessToken = await refreshAccessToken(oauthConfig, tokenStore, organizationId);
81502
81978
  }
81503
81979
  const linear2 = new LinearServiceImpl(accessToken);
81504
- const resolveResult = await resolveRepoPath(linear2, issueId, config2.projectsPath);
81505
- if (Result.isError(resolveResult)) {
81506
- log.error("Failed to resolve repository", {
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
81980
+ await dispatchAgentSessionEvent(event, linear2, opencode2, sessionRepository, {
81981
+ organizationId,
81982
+ projectsPath: config2.projectsPath
81516
81983
  });
81517
- const processor = new LinearEventProcessor(opencode2, linear2, sessionRepository, resolved.path, {
81518
- organizationId
81519
- });
81520
- await processor.process(event);
81521
81984
  }
81522
81985
  };
81523
81986
  }
@@ -81651,11 +82114,13 @@ Make sure OpenCode is running: opencode serve
81651
82114
  `);
81652
82115
  return server2;
81653
82116
  }
81654
- main().catch((error48) => {
81655
- const log = Log.create({ service: "startup" });
81656
- log.error("Failed to start server", {
81657
- error: error48 instanceof Error ? error48.message : String(error48),
81658
- stack: error48 instanceof Error ? error48.stack : undefined
82117
+ if (import.meta.main) {
82118
+ main().catch((error48) => {
82119
+ const log = Log.create({ service: "startup" });
82120
+ log.error("Failed to start server", {
82121
+ error: error48 instanceof Error ? error48.message : String(error48),
82122
+ stack: error48 instanceof Error ? error48.stack : undefined
82123
+ });
82124
+ process.exit(1);
81659
82125
  });
81660
- process.exit(1);
81661
- });
82126
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-linear-agent/server",
3
- "version": "0.1.3-master.11",
3
+ "version": "0.1.3-master.14",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {