@opencode-linear-agent/server 0.1.3-master.15 → 0.1.3-master.17

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 +128 -131
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -66902,7 +66902,7 @@ function toIssueStateType(value) {
66902
66902
  case "canceled":
66903
66903
  return value;
66904
66904
  default:
66905
- return "unstarted";
66905
+ return "unknown";
66906
66906
  }
66907
66907
  }
66908
66908
  function mapElicitationSignal(signal) {
@@ -67240,6 +67240,20 @@ ${truncatedStack}
67240
67240
  const result = await Result.tryPromise({
67241
67241
  try: async () => {
67242
67242
  const issue = await this.client.issue(issueId);
67243
+ const state = await issue.state;
67244
+ if (!state) {
67245
+ throw new Error("Issue has no workflow state");
67246
+ }
67247
+ const stateType = toIssueStateType(state.type);
67248
+ if (stateType !== "unstarted") {
67249
+ this.log.info("Skipping issue auto-transition", {
67250
+ issueId,
67251
+ stateId: state.id,
67252
+ stateName: state.name,
67253
+ stateType
67254
+ });
67255
+ return;
67256
+ }
67243
67257
  const team = await issue.team;
67244
67258
  if (!team) {
67245
67259
  throw new Error("Issue has no associated team");
@@ -67256,8 +67270,10 @@ ${truncatedStack}
67256
67270
  this.log.warn("No In Progress state found", { issueId });
67257
67271
  return;
67258
67272
  }
67259
- this.log.info("Moving issue to In Progress", {
67273
+ this.log.info("Moving issue to first started state", {
67260
67274
  issueId,
67275
+ fromStateId: state.id,
67276
+ fromStateName: state.name,
67261
67277
  stateId: inProgressState.id,
67262
67278
  stateName: inProgressState.name
67263
67279
  });
@@ -80973,7 +80989,7 @@ function generateSuccessHtml(result) {
80973
80989
 
80974
80990
  <h2>Next Steps:</h2>
80975
80991
  <ul>
80976
- <li><strong>Optional:</strong> set <code>LINEAR_ORGANIZATION_ID</code> to restrict to one org:
80992
+ <li><strong>Optional:</strong> set <code>linearOrganizationId</code> in <code>~/.config/opencode-linear-agent/config.json</code> to restrict to one org:
80977
80993
  <br><code>${result.organizationId}</code>
80978
80994
  </li>
80979
80995
  <li>Make sure your webhook URL is configured in Linear:
@@ -81322,88 +81338,91 @@ class OpencodeService {
81322
81338
  }
81323
81339
  // src/config.ts
81324
81340
  import { homedir } from "os";
81325
- import { resolve, join as join2 } from "path";
81341
+ import path2, { resolve } from "path";
81342
+
81343
+ // ../../node_modules/.bun/xdg-basedir@5.1.0/node_modules/xdg-basedir/index.js
81344
+ import os from "os";
81345
+ import path from "path";
81346
+ var homeDirectory = os.homedir();
81347
+ var { env } = process;
81348
+ var xdgData = env.XDG_DATA_HOME || (homeDirectory ? path.join(homeDirectory, ".local", "share") : undefined);
81349
+ var xdgConfig = env.XDG_CONFIG_HOME || (homeDirectory ? path.join(homeDirectory, ".config") : undefined);
81350
+ var xdgState = env.XDG_STATE_HOME || (homeDirectory ? path.join(homeDirectory, ".local", "state") : undefined);
81351
+ var xdgCache = env.XDG_CACHE_HOME || (homeDirectory ? path.join(homeDirectory, ".cache") : undefined);
81352
+ var xdgRuntime = env.XDG_RUNTIME_DIR || undefined;
81353
+ var xdgDataDirectories = (env.XDG_DATA_DIRS || "/usr/local/share/:/usr/share/").split(":");
81354
+ if (xdgData) {
81355
+ xdgDataDirectories.unshift(xdgData);
81356
+ }
81357
+ var xdgConfigDirectories = (env.XDG_CONFIG_DIRS || "/etc/xdg").split(":");
81358
+ if (xdgConfig) {
81359
+ xdgConfigDirectories.unshift(xdgConfig);
81360
+ }
81361
+
81362
+ // src/config.ts
81363
+ import { existsSync as existsSync3, readFileSync } from "fs";
81326
81364
  var DEFAULT_WEBHOOK_IPS = [
81327
81365
  "35.231.147.226",
81328
81366
  "35.243.134.228",
81329
81367
  "34.145.29.68"
81330
81368
  ];
81331
- var ConfigSchema = exports_external.object({
81332
- port: exports_external.coerce.number(),
81333
- publicHostname: exports_external.string(),
81334
- opencode: exports_external.object({
81335
- url: exports_external.string()
81336
- }),
81337
- linear: exports_external.object({
81338
- clientId: exports_external.string(),
81339
- clientSecret: exports_external.string(),
81340
- webhookSecret: exports_external.string(),
81341
- organizationId: exports_external.string().optional(),
81342
- webhookIps: exports_external.array(exports_external.string()).min(1)
81343
- }),
81344
- projectsPath: exports_external.string()
81369
+ var ConfigFileSchema = exports_external.object({
81370
+ webhookServerPublicHostname: exports_external.string().min(1),
81371
+ webhookServerPort: exports_external.coerce.number().default(3210),
81372
+ opencodeServerUrl: exports_external.string().min(1).default("http://localhost:4096"),
81373
+ linearClientId: exports_external.string().min(1),
81374
+ linearClientSecret: exports_external.string().min(1),
81375
+ linearWebhookSecret: exports_external.string().min(1),
81376
+ linearWebhookIps: exports_external.array(exports_external.string()).min(1).default(DEFAULT_WEBHOOK_IPS),
81377
+ linearOrganizationId: exports_external.string().optional(),
81378
+ projectsPath: exports_external.string().min(1).transform((projectsPath) => {
81379
+ if (projectsPath.startsWith("~/")) {
81380
+ return resolve(homedir(), projectsPath.slice(2));
81381
+ }
81382
+ return projectsPath;
81383
+ })
81345
81384
  });
81346
- function expandPath(path) {
81347
- if (path.startsWith("~/")) {
81348
- return resolve(homedir(), path.slice(2));
81385
+ var APPLICATION_DIRECTORY = "opencode-linear-agent";
81386
+ function loadConfig() {
81387
+ if (!xdgConfig) {
81388
+ throw new Error("Failed to find directory for config storage. Please ensure HOME or XDG_CONFIG_HOME environment variable is set.");
81349
81389
  }
81350
- return path;
81351
- }
81352
- function requiredEnv(name) {
81353
- const value = process.env[name];
81354
- if (!value) {
81355
- throw new Error(`Missing required environment variable: ${name}`);
81390
+ const configPath = path2.join(xdgConfig, APPLICATION_DIRECTORY, "config.json");
81391
+ if (!existsSync3(configPath)) {
81392
+ throw new Error(`Config file not found at ${configPath}. Please create a config file with the necessary configuration values.`);
81356
81393
  }
81357
- return value;
81358
- }
81359
- function loadConfig() {
81360
- const log = Log.create({ service: "config" });
81361
- log.info("Loading config from environment variables");
81362
- const webhookIpsRaw = process.env["LINEAR_WEBHOOK_IPS"];
81363
- const webhookIps = webhookIpsRaw ? webhookIpsRaw.split(",").map((ip) => ip.trim()) : DEFAULT_WEBHOOK_IPS;
81364
- const raw = {
81365
- port: process.env["PORT"] ?? "3210",
81366
- publicHostname: requiredEnv("PUBLIC_HOSTNAME"),
81367
- opencode: {
81368
- url: process.env["OPENCODE_URL"] ?? "http://localhost:4096"
81369
- },
81370
- linear: {
81371
- clientId: requiredEnv("LINEAR_CLIENT_ID"),
81372
- clientSecret: requiredEnv("LINEAR_CLIENT_SECRET"),
81373
- webhookSecret: requiredEnv("LINEAR_WEBHOOK_SECRET"),
81374
- organizationId: process.env["LINEAR_ORGANIZATION_ID"],
81375
- webhookIps
81376
- },
81377
- projectsPath: requiredEnv("PROJECTS_PATH")
81378
- };
81379
- const result = ConfigSchema.safeParse(raw);
81394
+ const rawConfig = readFileSync(configPath, "utf-8");
81395
+ let raw;
81396
+ try {
81397
+ raw = JSON.parse(rawConfig);
81398
+ } catch (err2) {
81399
+ throw new Error(`Failed to parse config file at ${configPath}`, {
81400
+ cause: err2
81401
+ });
81402
+ }
81403
+ const result = ConfigFileSchema.safeParse(raw);
81380
81404
  if (!result.success) {
81381
81405
  const issues = result.error.issues.map((issue2) => ` - ${issue2.path.join(".")}: ${issue2.message}`).join(`
81382
81406
  `);
81383
81407
  throw new Error(`Invalid configuration:
81384
81408
  ${issues}`);
81385
81409
  }
81386
- return {
81387
- ...result.data,
81388
- projectsPath: expandPath(result.data.projectsPath)
81389
- };
81390
- }
81391
- function getWorkerUrl(config2) {
81392
- return `https://${config2.publicHostname}`;
81393
- }
81394
- function getDataDir() {
81395
- return join2(homedir(), ".local/share/opencode-linear-agent");
81410
+ return result.data;
81396
81411
  }
81397
81412
 
81398
81413
  // src/storage/FileStore.ts
81399
- import { mkdir } from "fs/promises";
81400
- import { dirname } from "path";
81414
+ import { mkdir, writeFile, readFile, exists } from "fs/promises";
81415
+ import { dirname, join as join2 } from "path";
81401
81416
  class FileStore {
81402
- filePath;
81403
81417
  data = {};
81404
81418
  loaded = false;
81405
- constructor(filePath) {
81406
- this.filePath = filePath;
81419
+ filePath;
81420
+ constructor() {
81421
+ if (!xdgData) {
81422
+ throw new Error("Failed to find directory for data storage. Please ensure HOME or XDG_DATA_HOME environment variable is set.");
81423
+ }
81424
+ const dataDir = join2(xdgData, APPLICATION_DIRECTORY, "store.json");
81425
+ this.filePath = dataDir;
81407
81426
  }
81408
81427
  async ensureLoaded() {
81409
81428
  if (this.loaded) {
@@ -81412,20 +81431,23 @@ class FileStore {
81412
81431
  await this.reload();
81413
81432
  }
81414
81433
  async reload() {
81415
- const file2 = Bun.file(this.filePath);
81416
- if (await file2.exists()) {
81417
- try {
81418
- const json2 = await file2.json();
81419
- this.data = parseStoreData(json2);
81420
- } catch {
81421
- this.data = {};
81422
- }
81434
+ if (!await exists(this.filePath)) {
81435
+ this.data = {};
81436
+ this.loaded = true;
81437
+ return;
81438
+ }
81439
+ const file2 = await readFile(this.filePath);
81440
+ try {
81441
+ const json2 = JSON.parse(file2.toString());
81442
+ this.data = parseStoreData(json2);
81443
+ } catch {
81444
+ this.data = {};
81423
81445
  }
81424
81446
  this.loaded = true;
81425
81447
  }
81426
81448
  async save() {
81427
81449
  await mkdir(dirname(this.filePath), { recursive: true });
81428
- await Bun.write(this.filePath, JSON.stringify(this.data, null, 2));
81450
+ await writeFile(this.filePath, JSON.stringify(this.data, null, 2));
81429
81451
  }
81430
81452
  isExpired(stored) {
81431
81453
  if (!stored.expires) {
@@ -81925,7 +81947,6 @@ async function dispatchAgentSessionEvent(event, linear2, opencode2, sessionRepos
81925
81947
  }
81926
81948
 
81927
81949
  // src/index.ts
81928
- import { join as join4 } from "path";
81929
81950
  function getClientIp(request) {
81930
81951
  const cfIp = request.headers.get("cf-connecting-ip");
81931
81952
  if (cfIp) {
@@ -81946,7 +81967,7 @@ function isAllowedIp(ip, allowlist) {
81946
81967
  }
81947
81968
  function createDirectDispatcher(config2, tokenStore, sessionRepository) {
81948
81969
  const opencodeClient = createOpencodeClient({
81949
- baseUrl: config2.opencode.url
81970
+ baseUrl: config2.opencodeServerUrl
81950
81971
  });
81951
81972
  const opencode2 = new OpencodeService(opencodeClient);
81952
81973
  return {
@@ -81956,8 +81977,8 @@ function createDirectDispatcher(config2, tokenStore, sessionRepository) {
81956
81977
  let accessToken2 = await tokenStore.getAccessToken(organizationId);
81957
81978
  if (!accessToken2) {
81958
81979
  const oauthConfig = {
81959
- clientId: config2.linear.clientId,
81960
- clientSecret: config2.linear.clientSecret
81980
+ clientId: config2.linearClientId,
81981
+ clientSecret: config2.linearClientSecret
81961
81982
  };
81962
81983
  accessToken2 = await refreshAccessToken(oauthConfig, tokenStore, organizationId);
81963
81984
  }
@@ -81971,8 +81992,8 @@ function createDirectDispatcher(config2, tokenStore, sessionRepository) {
81971
81992
  if (!accessToken) {
81972
81993
  Log.create({ service: "dispatcher" }).tag("organizationId", organizationId).info("No access token, attempting refresh");
81973
81994
  const oauthConfig = {
81974
- clientId: config2.linear.clientId,
81975
- clientSecret: config2.linear.clientSecret
81995
+ clientId: config2.linearClientId,
81996
+ clientSecret: config2.linearClientSecret
81976
81997
  };
81977
81998
  accessToken = await refreshAccessToken(oauthConfig, tokenStore, organizationId);
81978
81999
  }
@@ -81986,12 +82007,12 @@ function createDirectDispatcher(config2, tokenStore, sessionRepository) {
81986
82007
  }
81987
82008
  function createServer(config2, kv, tokenStore, dispatcher) {
81988
82009
  const oauthConfig = {
81989
- clientId: config2.linear.clientId,
81990
- clientSecret: config2.linear.clientSecret,
81991
- baseUrl: getWorkerUrl(config2)
82010
+ clientId: config2.linearClientId,
82011
+ clientSecret: config2.linearClientSecret,
82012
+ baseUrl: `https://${config2.webhookServerPublicHostname}`
81992
82013
  };
81993
82014
  return Bun.serve({
81994
- port: config2.port,
82015
+ port: config2.webhookServerPort,
81995
82016
  async fetch(request) {
81996
82017
  const url2 = new URL(request.url);
81997
82018
  const pathname = url2.pathname;
@@ -82006,24 +82027,24 @@ function createServer(config2, kv, tokenStore, dispatcher) {
82006
82027
  });
82007
82028
  return response;
82008
82029
  };
82009
- if (pathname === "/api/oauth/authorize") {
82010
- return respond(await handleAuthorize(request, oauthConfig, kv));
82011
- }
82012
82030
  if (pathname === "/health") {
82013
82031
  return respond(new Response("OK", { status: 200 }));
82014
82032
  }
82033
+ if (pathname === "/api/oauth/authorize") {
82034
+ return respond(await handleAuthorize(request, oauthConfig, kv));
82035
+ }
82015
82036
  if (pathname === "/api/oauth/callback") {
82016
82037
  return respond(await handleCallback(request, oauthConfig, kv, tokenStore));
82017
82038
  }
82018
- if (pathname === "/api/webhook/linear" || pathname === "/webhook/linear") {
82019
- if (!isAllowedIp(clientIp, config2.linear.webhookIps)) {
82039
+ if (pathname === "/api/webhook/linear") {
82040
+ if (!isAllowedIp(clientIp, config2.linearWebhookIps)) {
82020
82041
  log.warn("Webhook request from unauthorized IP", {
82021
82042
  clientIp,
82022
- allowedIps: config2.linear.webhookIps
82043
+ allowedIps: config2.linearWebhookIps
82023
82044
  });
82024
82045
  return respond(new Response("Forbidden", { status: 403 }));
82025
82046
  }
82026
- return respond(await handleWebhook(request, config2.linear.webhookSecret, tokenStore, dispatcher, undefined, config2.linear.organizationId));
82047
+ return respond(await handleWebhook(request, config2.linearWebhookSecret, tokenStore, dispatcher, undefined, config2.linearOrganizationId));
82027
82048
  }
82028
82049
  return respond(new Response("Not found", { status: 404 }));
82029
82050
  }
@@ -82031,14 +82052,14 @@ function createServer(config2, kv, tokenStore, dispatcher) {
82031
82052
  }
82032
82053
  function startTokenRefreshTimer(config2, tokenStore) {
82033
82054
  const log = Log.create({ service: "token-refresh" });
82034
- const organizationId = config2.linear.organizationId;
82055
+ const organizationId = config2.linearOrganizationId;
82035
82056
  if (!organizationId) {
82036
82057
  log.info("Skipping proactive token refresh (LINEAR_ORGANIZATION_ID not set)");
82037
82058
  return;
82038
82059
  }
82039
82060
  const oauthConfig = {
82040
- clientId: config2.linear.clientId,
82041
- clientSecret: config2.linear.clientSecret
82061
+ clientId: config2.linearClientId,
82062
+ clientSecret: config2.linearClientSecret
82042
82063
  };
82043
82064
  const REFRESH_INTERVAL_MS = 20 * 60 * 60 * 1000;
82044
82065
  const refresh = async () => {
@@ -82054,61 +82075,37 @@ function startTokenRefreshTimer(config2, tokenStore) {
82054
82075
  refresh();
82055
82076
  setInterval(() => void refresh(), REFRESH_INTERVAL_MS);
82056
82077
  }
82057
- async function validateStoreFile(filePath, log) {
82058
- const file2 = Bun.file(filePath);
82059
- if (!await file2.exists()) {
82060
- return;
82061
- }
82062
- const result = await Result.tryPromise({
82063
- try: async () => {
82064
- const json2 = await file2.json();
82065
- parseStoreData(json2);
82066
- },
82067
- catch: (e) => e instanceof Error ? e.message : String(e)
82068
- });
82069
- if (Result.isError(result)) {
82070
- log.warn("Invalid shared store file detected", {
82071
- dataPath: filePath,
82072
- error: result.error,
82073
- recovery: "Fix/restore store.json, restart server, then re-auth Linear if token data was lost."
82074
- });
82075
- }
82076
- }
82077
82078
  async function main() {
82078
82079
  const log = Log.create({ service: "startup" });
82079
82080
  log.info("Starting Linear OpenCode Agent (Local)");
82080
82081
  const config2 = loadConfig();
82081
82082
  log.info("Configuration loaded", {
82082
- port: config2.port,
82083
- publicHostname: config2.publicHostname,
82084
- opencodeUrl: config2.opencode.url,
82083
+ port: config2.webhookServerPort,
82084
+ publicHostname: config2.webhookServerPublicHostname,
82085
+ opencodeUrl: config2.opencodeServerUrl,
82085
82086
  projectsPath: config2.projectsPath
82086
82087
  });
82087
- const dataDir = getDataDir();
82088
- const dataPath = join4(dataDir, "store.json");
82089
- const kv = new FileStore(dataPath);
82088
+ const kv = new FileStore;
82090
82089
  const tokenStore = new FileTokenStore(kv);
82091
82090
  const sessionRepository = new FileSessionRepository(kv);
82092
- log.info("Storage initialized", { dataPath });
82093
- await validateStoreFile(dataPath, log);
82094
82091
  startTokenRefreshTimer(config2, tokenStore);
82095
82092
  const dispatcher = createDirectDispatcher(config2, tokenStore, sessionRepository);
82096
82093
  const server2 = createServer(config2, kv, tokenStore, dispatcher);
82097
- const workerUrl = getWorkerUrl(config2);
82094
+ const webhookServerUrl = `https://${config2.webhookServerPublicHostname}`;
82098
82095
  log.info("Server started", {
82099
- port: config2.port,
82100
- workerUrl,
82101
- webhookUrl: `${workerUrl}/api/webhook/linear`,
82102
- oauthUrl: `${workerUrl}/api/oauth/authorize`
82096
+ port: config2.webhookServerPort,
82097
+ webhookServerUrl,
82098
+ webhookUrl: `${webhookServerUrl}/api/webhook/linear`,
82099
+ oauthUrl: `${webhookServerUrl}/api/oauth/authorize`
82103
82100
  });
82104
82101
  process.stdout.write(`
82105
82102
  Linear OpenCode Agent (Local) running!
82106
82103
 
82107
- Local: http://localhost:${config2.port}
82108
- Public: ${workerUrl}
82104
+ Local: http://localhost:${config2.webhookServerPort}
82105
+ Public: ${webhookServerUrl}
82109
82106
 
82110
- Webhook URL: ${workerUrl}/api/webhook/linear
82111
- OAuth URL: ${workerUrl}/api/oauth/authorize
82107
+ Webhook URL: ${webhookServerUrl}/api/webhook/linear
82108
+ OAuth URL: ${webhookServerUrl}/api/oauth/authorize
82112
82109
 
82113
82110
  Make sure OpenCode is running: opencode serve
82114
82111
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-linear-agent/server",
3
- "version": "0.1.3-master.15",
3
+ "version": "0.1.3-master.17",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -33,6 +33,7 @@
33
33
  "@linear/sdk": "catalog:",
34
34
  "@opencode-ai/sdk": "catalog:",
35
35
  "better-result": "catalog:",
36
+ "xdg-basedir": "catalog:",
36
37
  "zod": "catalog:"
37
38
  },
38
39
  "peerDependencies": {