@opencode-linear-agent/server 0.1.3-master.16 → 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 +110 -129
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -80989,7 +80989,7 @@ function generateSuccessHtml(result) {
80989
80989
 
80990
80990
  <h2>Next Steps:</h2>
80991
80991
  <ul>
80992
- <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:
80993
80993
  <br><code>${result.organizationId}</code>
80994
80994
  </li>
80995
80995
  <li>Make sure your webhook URL is configured in Linear:
@@ -81338,88 +81338,91 @@ class OpencodeService {
81338
81338
  }
81339
81339
  // src/config.ts
81340
81340
  import { homedir } from "os";
81341
- 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";
81342
81364
  var DEFAULT_WEBHOOK_IPS = [
81343
81365
  "35.231.147.226",
81344
81366
  "35.243.134.228",
81345
81367
  "34.145.29.68"
81346
81368
  ];
81347
- var ConfigSchema = exports_external.object({
81348
- port: exports_external.coerce.number(),
81349
- publicHostname: exports_external.string(),
81350
- opencode: exports_external.object({
81351
- url: exports_external.string()
81352
- }),
81353
- linear: exports_external.object({
81354
- clientId: exports_external.string(),
81355
- clientSecret: exports_external.string(),
81356
- webhookSecret: exports_external.string(),
81357
- organizationId: exports_external.string().optional(),
81358
- webhookIps: exports_external.array(exports_external.string()).min(1)
81359
- }),
81360
- 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
+ })
81361
81384
  });
81362
- function expandPath(path) {
81363
- if (path.startsWith("~/")) {
81364
- 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.");
81365
81389
  }
81366
- return path;
81367
- }
81368
- function requiredEnv(name) {
81369
- const value = process.env[name];
81370
- if (!value) {
81371
- 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.`);
81372
81393
  }
81373
- return value;
81374
- }
81375
- function loadConfig() {
81376
- const log = Log.create({ service: "config" });
81377
- log.info("Loading config from environment variables");
81378
- const webhookIpsRaw = process.env["LINEAR_WEBHOOK_IPS"];
81379
- const webhookIps = webhookIpsRaw ? webhookIpsRaw.split(",").map((ip) => ip.trim()) : DEFAULT_WEBHOOK_IPS;
81380
- const raw = {
81381
- port: process.env["PORT"] ?? "3210",
81382
- publicHostname: requiredEnv("PUBLIC_HOSTNAME"),
81383
- opencode: {
81384
- url: process.env["OPENCODE_URL"] ?? "http://localhost:4096"
81385
- },
81386
- linear: {
81387
- clientId: requiredEnv("LINEAR_CLIENT_ID"),
81388
- clientSecret: requiredEnv("LINEAR_CLIENT_SECRET"),
81389
- webhookSecret: requiredEnv("LINEAR_WEBHOOK_SECRET"),
81390
- organizationId: process.env["LINEAR_ORGANIZATION_ID"],
81391
- webhookIps
81392
- },
81393
- projectsPath: requiredEnv("PROJECTS_PATH")
81394
- };
81395
- 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);
81396
81404
  if (!result.success) {
81397
81405
  const issues = result.error.issues.map((issue2) => ` - ${issue2.path.join(".")}: ${issue2.message}`).join(`
81398
81406
  `);
81399
81407
  throw new Error(`Invalid configuration:
81400
81408
  ${issues}`);
81401
81409
  }
81402
- return {
81403
- ...result.data,
81404
- projectsPath: expandPath(result.data.projectsPath)
81405
- };
81406
- }
81407
- function getWorkerUrl(config2) {
81408
- return `https://${config2.publicHostname}`;
81409
- }
81410
- function getDataDir() {
81411
- return join2(homedir(), ".local/share/opencode-linear-agent");
81410
+ return result.data;
81412
81411
  }
81413
81412
 
81414
81413
  // src/storage/FileStore.ts
81415
- import { mkdir } from "fs/promises";
81416
- import { dirname } from "path";
81414
+ import { mkdir, writeFile, readFile, exists } from "fs/promises";
81415
+ import { dirname, join as join2 } from "path";
81417
81416
  class FileStore {
81418
- filePath;
81419
81417
  data = {};
81420
81418
  loaded = false;
81421
- constructor(filePath) {
81422
- 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;
81423
81426
  }
81424
81427
  async ensureLoaded() {
81425
81428
  if (this.loaded) {
@@ -81428,20 +81431,23 @@ class FileStore {
81428
81431
  await this.reload();
81429
81432
  }
81430
81433
  async reload() {
81431
- const file2 = Bun.file(this.filePath);
81432
- if (await file2.exists()) {
81433
- try {
81434
- const json2 = await file2.json();
81435
- this.data = parseStoreData(json2);
81436
- } catch {
81437
- this.data = {};
81438
- }
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 = {};
81439
81445
  }
81440
81446
  this.loaded = true;
81441
81447
  }
81442
81448
  async save() {
81443
81449
  await mkdir(dirname(this.filePath), { recursive: true });
81444
- await Bun.write(this.filePath, JSON.stringify(this.data, null, 2));
81450
+ await writeFile(this.filePath, JSON.stringify(this.data, null, 2));
81445
81451
  }
81446
81452
  isExpired(stored) {
81447
81453
  if (!stored.expires) {
@@ -81941,7 +81947,6 @@ async function dispatchAgentSessionEvent(event, linear2, opencode2, sessionRepos
81941
81947
  }
81942
81948
 
81943
81949
  // src/index.ts
81944
- import { join as join4 } from "path";
81945
81950
  function getClientIp(request) {
81946
81951
  const cfIp = request.headers.get("cf-connecting-ip");
81947
81952
  if (cfIp) {
@@ -81962,7 +81967,7 @@ function isAllowedIp(ip, allowlist) {
81962
81967
  }
81963
81968
  function createDirectDispatcher(config2, tokenStore, sessionRepository) {
81964
81969
  const opencodeClient = createOpencodeClient({
81965
- baseUrl: config2.opencode.url
81970
+ baseUrl: config2.opencodeServerUrl
81966
81971
  });
81967
81972
  const opencode2 = new OpencodeService(opencodeClient);
81968
81973
  return {
@@ -81972,8 +81977,8 @@ function createDirectDispatcher(config2, tokenStore, sessionRepository) {
81972
81977
  let accessToken2 = await tokenStore.getAccessToken(organizationId);
81973
81978
  if (!accessToken2) {
81974
81979
  const oauthConfig = {
81975
- clientId: config2.linear.clientId,
81976
- clientSecret: config2.linear.clientSecret
81980
+ clientId: config2.linearClientId,
81981
+ clientSecret: config2.linearClientSecret
81977
81982
  };
81978
81983
  accessToken2 = await refreshAccessToken(oauthConfig, tokenStore, organizationId);
81979
81984
  }
@@ -81987,8 +81992,8 @@ function createDirectDispatcher(config2, tokenStore, sessionRepository) {
81987
81992
  if (!accessToken) {
81988
81993
  Log.create({ service: "dispatcher" }).tag("organizationId", organizationId).info("No access token, attempting refresh");
81989
81994
  const oauthConfig = {
81990
- clientId: config2.linear.clientId,
81991
- clientSecret: config2.linear.clientSecret
81995
+ clientId: config2.linearClientId,
81996
+ clientSecret: config2.linearClientSecret
81992
81997
  };
81993
81998
  accessToken = await refreshAccessToken(oauthConfig, tokenStore, organizationId);
81994
81999
  }
@@ -82002,12 +82007,12 @@ function createDirectDispatcher(config2, tokenStore, sessionRepository) {
82002
82007
  }
82003
82008
  function createServer(config2, kv, tokenStore, dispatcher) {
82004
82009
  const oauthConfig = {
82005
- clientId: config2.linear.clientId,
82006
- clientSecret: config2.linear.clientSecret,
82007
- baseUrl: getWorkerUrl(config2)
82010
+ clientId: config2.linearClientId,
82011
+ clientSecret: config2.linearClientSecret,
82012
+ baseUrl: `https://${config2.webhookServerPublicHostname}`
82008
82013
  };
82009
82014
  return Bun.serve({
82010
- port: config2.port,
82015
+ port: config2.webhookServerPort,
82011
82016
  async fetch(request) {
82012
82017
  const url2 = new URL(request.url);
82013
82018
  const pathname = url2.pathname;
@@ -82022,24 +82027,24 @@ function createServer(config2, kv, tokenStore, dispatcher) {
82022
82027
  });
82023
82028
  return response;
82024
82029
  };
82025
- if (pathname === "/api/oauth/authorize") {
82026
- return respond(await handleAuthorize(request, oauthConfig, kv));
82027
- }
82028
82030
  if (pathname === "/health") {
82029
82031
  return respond(new Response("OK", { status: 200 }));
82030
82032
  }
82033
+ if (pathname === "/api/oauth/authorize") {
82034
+ return respond(await handleAuthorize(request, oauthConfig, kv));
82035
+ }
82031
82036
  if (pathname === "/api/oauth/callback") {
82032
82037
  return respond(await handleCallback(request, oauthConfig, kv, tokenStore));
82033
82038
  }
82034
- if (pathname === "/api/webhook/linear" || pathname === "/webhook/linear") {
82035
- if (!isAllowedIp(clientIp, config2.linear.webhookIps)) {
82039
+ if (pathname === "/api/webhook/linear") {
82040
+ if (!isAllowedIp(clientIp, config2.linearWebhookIps)) {
82036
82041
  log.warn("Webhook request from unauthorized IP", {
82037
82042
  clientIp,
82038
- allowedIps: config2.linear.webhookIps
82043
+ allowedIps: config2.linearWebhookIps
82039
82044
  });
82040
82045
  return respond(new Response("Forbidden", { status: 403 }));
82041
82046
  }
82042
- 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));
82043
82048
  }
82044
82049
  return respond(new Response("Not found", { status: 404 }));
82045
82050
  }
@@ -82047,14 +82052,14 @@ function createServer(config2, kv, tokenStore, dispatcher) {
82047
82052
  }
82048
82053
  function startTokenRefreshTimer(config2, tokenStore) {
82049
82054
  const log = Log.create({ service: "token-refresh" });
82050
- const organizationId = config2.linear.organizationId;
82055
+ const organizationId = config2.linearOrganizationId;
82051
82056
  if (!organizationId) {
82052
82057
  log.info("Skipping proactive token refresh (LINEAR_ORGANIZATION_ID not set)");
82053
82058
  return;
82054
82059
  }
82055
82060
  const oauthConfig = {
82056
- clientId: config2.linear.clientId,
82057
- clientSecret: config2.linear.clientSecret
82061
+ clientId: config2.linearClientId,
82062
+ clientSecret: config2.linearClientSecret
82058
82063
  };
82059
82064
  const REFRESH_INTERVAL_MS = 20 * 60 * 60 * 1000;
82060
82065
  const refresh = async () => {
@@ -82070,61 +82075,37 @@ function startTokenRefreshTimer(config2, tokenStore) {
82070
82075
  refresh();
82071
82076
  setInterval(() => void refresh(), REFRESH_INTERVAL_MS);
82072
82077
  }
82073
- async function validateStoreFile(filePath, log) {
82074
- const file2 = Bun.file(filePath);
82075
- if (!await file2.exists()) {
82076
- return;
82077
- }
82078
- const result = await Result.tryPromise({
82079
- try: async () => {
82080
- const json2 = await file2.json();
82081
- parseStoreData(json2);
82082
- },
82083
- catch: (e) => e instanceof Error ? e.message : String(e)
82084
- });
82085
- if (Result.isError(result)) {
82086
- log.warn("Invalid shared store file detected", {
82087
- dataPath: filePath,
82088
- error: result.error,
82089
- recovery: "Fix/restore store.json, restart server, then re-auth Linear if token data was lost."
82090
- });
82091
- }
82092
- }
82093
82078
  async function main() {
82094
82079
  const log = Log.create({ service: "startup" });
82095
82080
  log.info("Starting Linear OpenCode Agent (Local)");
82096
82081
  const config2 = loadConfig();
82097
82082
  log.info("Configuration loaded", {
82098
- port: config2.port,
82099
- publicHostname: config2.publicHostname,
82100
- opencodeUrl: config2.opencode.url,
82083
+ port: config2.webhookServerPort,
82084
+ publicHostname: config2.webhookServerPublicHostname,
82085
+ opencodeUrl: config2.opencodeServerUrl,
82101
82086
  projectsPath: config2.projectsPath
82102
82087
  });
82103
- const dataDir = getDataDir();
82104
- const dataPath = join4(dataDir, "store.json");
82105
- const kv = new FileStore(dataPath);
82088
+ const kv = new FileStore;
82106
82089
  const tokenStore = new FileTokenStore(kv);
82107
82090
  const sessionRepository = new FileSessionRepository(kv);
82108
- log.info("Storage initialized", { dataPath });
82109
- await validateStoreFile(dataPath, log);
82110
82091
  startTokenRefreshTimer(config2, tokenStore);
82111
82092
  const dispatcher = createDirectDispatcher(config2, tokenStore, sessionRepository);
82112
82093
  const server2 = createServer(config2, kv, tokenStore, dispatcher);
82113
- const workerUrl = getWorkerUrl(config2);
82094
+ const webhookServerUrl = `https://${config2.webhookServerPublicHostname}`;
82114
82095
  log.info("Server started", {
82115
- port: config2.port,
82116
- workerUrl,
82117
- webhookUrl: `${workerUrl}/api/webhook/linear`,
82118
- oauthUrl: `${workerUrl}/api/oauth/authorize`
82096
+ port: config2.webhookServerPort,
82097
+ webhookServerUrl,
82098
+ webhookUrl: `${webhookServerUrl}/api/webhook/linear`,
82099
+ oauthUrl: `${webhookServerUrl}/api/oauth/authorize`
82119
82100
  });
82120
82101
  process.stdout.write(`
82121
82102
  Linear OpenCode Agent (Local) running!
82122
82103
 
82123
- Local: http://localhost:${config2.port}
82124
- Public: ${workerUrl}
82104
+ Local: http://localhost:${config2.webhookServerPort}
82105
+ Public: ${webhookServerUrl}
82125
82106
 
82126
- Webhook URL: ${workerUrl}/api/webhook/linear
82127
- OAuth URL: ${workerUrl}/api/oauth/authorize
82107
+ Webhook URL: ${webhookServerUrl}/api/webhook/linear
82108
+ OAuth URL: ${webhookServerUrl}/api/oauth/authorize
82128
82109
 
82129
82110
  Make sure OpenCode is running: opencode serve
82130
82111
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-linear-agent/server",
3
- "version": "0.1.3-master.16",
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": {