@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.
- package/dist/index.js +110 -129
- 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>
|
|
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
|
|
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
|
|
81348
|
-
|
|
81349
|
-
|
|
81350
|
-
|
|
81351
|
-
|
|
81352
|
-
|
|
81353
|
-
|
|
81354
|
-
|
|
81355
|
-
|
|
81356
|
-
|
|
81357
|
-
|
|
81358
|
-
|
|
81359
|
-
|
|
81360
|
-
|
|
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
|
-
|
|
81363
|
-
|
|
81364
|
-
|
|
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
|
-
|
|
81367
|
-
|
|
81368
|
-
|
|
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
|
-
|
|
81374
|
-
|
|
81375
|
-
|
|
81376
|
-
|
|
81377
|
-
|
|
81378
|
-
|
|
81379
|
-
|
|
81380
|
-
|
|
81381
|
-
|
|
81382
|
-
|
|
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
|
-
|
|
81422
|
-
|
|
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
|
-
|
|
81432
|
-
|
|
81433
|
-
|
|
81434
|
-
|
|
81435
|
-
|
|
81436
|
-
|
|
81437
|
-
|
|
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
|
|
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.
|
|
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.
|
|
81976
|
-
clientSecret: config2.
|
|
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.
|
|
81991
|
-
clientSecret: config2.
|
|
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.
|
|
82006
|
-
clientSecret: config2.
|
|
82007
|
-
baseUrl:
|
|
82010
|
+
clientId: config2.linearClientId,
|
|
82011
|
+
clientSecret: config2.linearClientSecret,
|
|
82012
|
+
baseUrl: `https://${config2.webhookServerPublicHostname}`
|
|
82008
82013
|
};
|
|
82009
82014
|
return Bun.serve({
|
|
82010
|
-
port: config2.
|
|
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"
|
|
82035
|
-
if (!isAllowedIp(clientIp, config2.
|
|
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.
|
|
82043
|
+
allowedIps: config2.linearWebhookIps
|
|
82039
82044
|
});
|
|
82040
82045
|
return respond(new Response("Forbidden", { status: 403 }));
|
|
82041
82046
|
}
|
|
82042
|
-
return respond(await handleWebhook(request, config2.
|
|
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.
|
|
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.
|
|
82057
|
-
clientSecret: config2.
|
|
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.
|
|
82099
|
-
publicHostname: config2.
|
|
82100
|
-
opencodeUrl: config2.
|
|
82083
|
+
port: config2.webhookServerPort,
|
|
82084
|
+
publicHostname: config2.webhookServerPublicHostname,
|
|
82085
|
+
opencodeUrl: config2.opencodeServerUrl,
|
|
82101
82086
|
projectsPath: config2.projectsPath
|
|
82102
82087
|
});
|
|
82103
|
-
const
|
|
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
|
|
82094
|
+
const webhookServerUrl = `https://${config2.webhookServerPublicHostname}`;
|
|
82114
82095
|
log.info("Server started", {
|
|
82115
|
-
port: config2.
|
|
82116
|
-
|
|
82117
|
-
webhookUrl: `${
|
|
82118
|
-
oauthUrl: `${
|
|
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.
|
|
82124
|
-
Public: ${
|
|
82104
|
+
Local: http://localhost:${config2.webhookServerPort}
|
|
82105
|
+
Public: ${webhookServerUrl}
|
|
82125
82106
|
|
|
82126
|
-
Webhook URL: ${
|
|
82127
|
-
OAuth URL: ${
|
|
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.
|
|
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": {
|