UNPKG

shippie

Version:
1,410 lines (1,400 loc) • 82.6 kB
import { createRequire } from "node:module"; import { local, sqlite } from "@flue/runtime/node"; import { Bash, InMemoryFs, bashFactoryToSessionEnv, configureFlueRuntime, createDefaultFlueApp, createFlueContext, createNodeAgentCoordinator, createNodeDispatchQueue, generateWorkflowRunId, invokeDirectAttached, invokeWorkflowAttached, resolveModel } from "@flue/runtime/internal"; import { connectMcpServer, createAgent, defineTool, dispatch } from "@flue/runtime"; import { createHash, randomUUID } from "node:crypto"; import { createGitHubChannel } from "@flue/github"; import { Octokit } from "octokit"; import * as v from "valibot"; import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises"; import { isAbsolute, join, relative } from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import picomatch from "picomatch"; //#region \0rolldown/runtime.js var __defProp = Object.defineProperty; var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports); var __exportAll = (all, no_symbols) => { let target = {}; for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" }); return target; }; var __require = /* @__PURE__ */ createRequire(import.meta.url); //#endregion //#region \0virtual:flue/packaged-skills var packagedSkills$1 = /* @__PURE__ */ new Map(); function getPackagedSkills() { return Object.fromEntries(packagedSkills$1); } //#endregion //#region node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs var require_constants_BXAKTxRC = /* @__PURE__ */ __commonJSMin(((exports) => { var X_ALREADY_SENT = "x-hono-already-sent"; Object.defineProperty(exports, "X_ALREADY_SENT", { enumerable: true, get: function() { return X_ALREADY_SENT; } }); })); //#endregion //#region node_modules/hono/dist/cjs/helper/websocket/index.js var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => { var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var websocket_exports = {}; __export(websocket_exports, { WSContext: () => WSContext, createWSMessageEvent: () => createWSMessageEvent, defineWebSocketHelper: () => defineWebSocketHelper }); module.exports = __toCommonJS(websocket_exports); var WSContext = class { #init; constructor(init) { this.#init = init; this.raw = init.raw; this.url = init.url ? new URL(init.url) : null; this.protocol = init.protocol ?? null; } send(source, options) { this.#init.send(source, options ?? {}); } raw; binaryType = "arraybuffer"; get readyState() { return this.#init.readyState; } url; protocol; close(code, reason) { this.#init.close(code, reason); } }; var createWSMessageEvent = (source) => { return new MessageEvent("message", { data: source }); }; var defineWebSocketHelper = (handler) => { return ((...args) => { if (typeof args[0] === "function") { const [createEvents, options] = args; return async function upgradeWebSocket(c, next) { const result = await handler(c, await createEvents(c), options); if (result) return result; await next(); }; } else { const [c, events, options] = args; return (async () => { const upgraded = await handler(c, events, options); if (!upgraded) throw new Error("Failed to upgrade WebSocket"); return upgraded; })(); } }); }; 0 && (module.exports = { WSContext, createWSMessageEvent, defineWebSocketHelper }); })); //#endregion //#region src/channels/github.ts var import_dist = (/* @__PURE__ */ __commonJSMin(((exports) => { Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); var require_constants = require_constants_BXAKTxRC(); var node_http = __require("node:http"); var node_http2 = __require("node:http2"); var node_stream = __require("node:stream"); var hono_ws = require_websocket(); var RequestError = class extends Error { constructor(message, options) { super(message, options); this.name = "RequestError"; } }; var reValidRequestUrl = /^\/[!#$&-;=?-\[\]_a-z~]*$/; var reDotSegment = /\/\.\.?(?:[/?#]|$)/; var reValidHost = /^[a-z0-9._-]+(?::(?:[1-5]\d{3,4}|[6-9]\d{3}))?$/; var buildUrl = (scheme, host, incomingUrl) => { const url = `${scheme}://${host}${incomingUrl}`; if (!reValidHost.test(host)) { const urlObj = new URL(url); if (urlObj.hostname.length !== host.length && urlObj.hostname !== (host.includes(":") ? host.replace(/:\d+$/, "") : host).toLowerCase()) throw new RequestError("Invalid host header"); return urlObj.href; } else if (incomingUrl.length === 0) return url + "/"; else { if (incomingUrl.charCodeAt(0) !== 47) throw new RequestError("Invalid URL"); if (!reValidRequestUrl.test(incomingUrl) || reDotSegment.test(incomingUrl)) return new URL(url).href; return url; } }; var toRequestError = (e) => { if (e instanceof RequestError) return e; return new RequestError(e.message, { cause: e }); }; var GlobalRequest = global.Request; var Request$1 = class extends GlobalRequest { constructor(input, options) { if (typeof input === "object" && getRequestCache in input) { const hasReplacementBody = options !== void 0 && "body" in options && options.body != null; if (input[bodyConsumedDirectlyKey] && !hasReplacementBody) throw new TypeError("Cannot construct a Request with a Request object that has already been used."); input = input[getRequestCache](); } if (typeof (options?.body)?.getReader !== "undefined") options.duplex ??= "half"; super(input, options); } }; var newHeadersFromIncoming = (incoming) => { const headerRecord = []; const rawHeaders = incoming.rawHeaders; for (let i = 0, len = rawHeaders.length; i < len; i += 2) { const key = rawHeaders[i]; if (key.charCodeAt(0) !== 58) headerRecord.push([key, rawHeaders[i + 1]]); } return new Headers(headerRecord); }; var wrapBodyStream = Symbol("wrapBodyStream"); var newRequestFromIncoming = (method, url, headers, incoming, abortController) => { const init = { method, headers, signal: abortController.signal }; if (method === "TRACE") { init.method = "GET"; const req = new Request$1(url, init); Object.defineProperty(req, "method", { get() { return "TRACE"; } }); return req; } if (!(method === "GET" || method === "HEAD")) if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) init.body = new ReadableStream({ start(controller) { controller.enqueue(incoming.rawBody); controller.close(); } }); else if (incoming[wrapBodyStream]) { let reader; init.body = new ReadableStream({ async pull(controller) { try { reader ||= node_stream.Readable.toWeb(incoming).getReader(); const { done, value } = await reader.read(); if (done) controller.close(); else controller.enqueue(value); } catch (error) { controller.error(error); } } }); } else init.body = node_stream.Readable.toWeb(incoming); return new Request$1(url, init); }; var getRequestCache = Symbol("getRequestCache"); var requestCache = Symbol("requestCache"); var incomingKey = Symbol("incomingKey"); var urlKey = Symbol("urlKey"); var methodKey = Symbol("methodKey"); var headersKey = Symbol("headersKey"); var abortControllerKey = Symbol("abortControllerKey"); var getAbortController = Symbol("getAbortController"); var abortRequest = Symbol("abortRequest"); var bodyBufferKey = Symbol("bodyBuffer"); var bodyReadPromiseKey = Symbol("bodyReadPromise"); var bodyConsumedDirectlyKey = Symbol("bodyConsumedDirectly"); var bodyLockReaderKey = Symbol("bodyLockReader"); var abortReasonKey = Symbol("abortReason"); var newBodyUnusableError = () => { return /* @__PURE__ */ new TypeError("Body is unusable"); }; var rejectBodyUnusable = () => { return Promise.reject(newBodyUnusableError()); }; var textDecoder = new TextDecoder(); var consumeBodyDirectOnce = (request) => { if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); request[bodyConsumedDirectlyKey] = true; }; var toArrayBuffer = (buf) => { return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); }; var contentType = (request) => { return (request[headersKey] ||= newHeadersFromIncoming(request[incomingKey])).get("content-type") || ""; }; var methodTokenRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; var normalizeIncomingMethod = (method) => { if (typeof method !== "string" || method.length === 0) return "GET"; switch (method) { case "DELETE": case "GET": case "HEAD": case "OPTIONS": case "POST": case "PUT": return method; } const upper = method.toUpperCase(); switch (upper) { case "DELETE": case "GET": case "HEAD": case "OPTIONS": case "POST": case "PUT": return upper; default: return method; } }; var validateDirectReadMethod = (method) => { if (!methodTokenRegExp.test(method)) return /* @__PURE__ */ new TypeError(`'${method}' is not a valid HTTP method.`); const normalized = method.toUpperCase(); if (normalized === "CONNECT" || normalized === "TRACK" || normalized === "TRACE" && method !== "TRACE") return /* @__PURE__ */ new TypeError(`'${method}' HTTP method is unsupported.`); }; var readBodyWithFastPath = (request, method, fromBuffer) => { if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); const methodName = request.method; if (methodName === "GET" || methodName === "HEAD") return request[getRequestCache]()[method](); const methodValidationError = validateDirectReadMethod(methodName); if (methodValidationError) return Promise.reject(methodValidationError); if (request[requestCache]) { if (methodName !== "TRACE") return request[requestCache][method](); } const alreadyUsedError = consumeBodyDirectOnce(request); if (alreadyUsedError) return alreadyUsedError; const raw = readRawBodyIfAvailable(request); if (raw) { const result = Promise.resolve(fromBuffer(raw, request)); request[bodyBufferKey] = void 0; return result; } return readBodyDirect(request).then((buf) => { const result = fromBuffer(buf, request); request[bodyBufferKey] = void 0; return result; }); }; var readRawBodyIfAvailable = (request) => { const incoming = request[incomingKey]; if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) return incoming.rawBody; }; var readBodyDirect = (request) => { if (request[bodyBufferKey]) return Promise.resolve(request[bodyBufferKey]); if (request[bodyReadPromiseKey]) return request[bodyReadPromiseKey]; const incoming = request[incomingKey]; if (node_stream.Readable.isDisturbed(incoming)) return rejectBodyUnusable(); const promise = new Promise((resolve, reject) => { const chunks = []; let settled = false; const finish = (callback) => { if (settled) return; settled = true; cleanup(); callback(); }; const onData = (chunk) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }; const onEnd = () => { finish(() => { const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks); request[bodyBufferKey] = buffer; resolve(buffer); }); }; const onError = (error) => { finish(() => { reject(error); }); }; const onClose = () => { if (incoming.readableEnded) { onEnd(); return; } finish(() => { if (incoming.errored) { reject(incoming.errored); return; } const reason = request[abortReasonKey]; if (reason !== void 0) { reject(reason instanceof Error ? reason : new Error(String(reason))); return; } reject(/* @__PURE__ */ new Error("Client connection prematurely closed.")); }); }; const cleanup = () => { incoming.off("data", onData); incoming.off("end", onEnd); incoming.off("error", onError); incoming.off("close", onClose); request[bodyReadPromiseKey] = void 0; }; incoming.on("data", onData); incoming.on("end", onEnd); incoming.on("error", onError); incoming.on("close", onClose); queueMicrotask(() => { if (settled) return; if (incoming.readableEnded) onEnd(); else if (incoming.errored) onError(incoming.errored); else if (incoming.destroyed) onClose(); }); }); request[bodyReadPromiseKey] = promise; return promise; }; var requestPrototype = { get method() { return this[methodKey]; }, get url() { return this[urlKey]; }, get headers() { return this[headersKey] ||= newHeadersFromIncoming(this[incomingKey]); }, [abortRequest](reason) { if (this[abortReasonKey] === void 0) this[abortReasonKey] = reason; const abortController = this[abortControllerKey]; if (abortController && !abortController.signal.aborted) abortController.abort(reason); }, [getAbortController]() { this[abortControllerKey] ||= new AbortController(); if (this[abortReasonKey] !== void 0 && !this[abortControllerKey].signal.aborted) this[abortControllerKey].abort(this[abortReasonKey]); return this[abortControllerKey]; }, [getRequestCache]() { const abortController = this[getAbortController](); if (this[requestCache]) return this[requestCache]; const method = this.method; if (this[bodyConsumedDirectlyKey] && !(method === "GET" || method === "HEAD")) { this[bodyBufferKey] = void 0; const init = { method: method === "TRACE" ? "GET" : method, headers: this.headers, signal: abortController.signal }; if (method !== "TRACE") { init.body = new ReadableStream({ start(c) { c.close(); } }); init.duplex = "half"; } const req = new Request$1(this[urlKey], init); if (method === "TRACE") Object.defineProperty(req, "method", { get() { return "TRACE"; } }); return this[requestCache] = req; } return this[requestCache] = newRequestFromIncoming(this.method, this[urlKey], this.headers, this[incomingKey], abortController); }, get body() { if (!this[bodyConsumedDirectlyKey]) return this[getRequestCache]().body; const request = this[getRequestCache](); if (!this[bodyLockReaderKey] && request.body) this[bodyLockReaderKey] = request.body.getReader(); return request.body; }, get bodyUsed() { if (this[bodyConsumedDirectlyKey]) return true; if (this[requestCache]) return this[requestCache].bodyUsed; return false; } }; Object.defineProperty(requestPrototype, "signal", { get() { return this[getAbortController]().signal; } }); [ "cache", "credentials", "destination", "integrity", "mode", "redirect", "referrer", "referrerPolicy", "keepalive" ].forEach((k) => { Object.defineProperty(requestPrototype, k, { get() { return this[getRequestCache]()[k]; } }); }); ["clone", "formData"].forEach((k) => { Object.defineProperty(requestPrototype, k, { value: function() { if (this[bodyConsumedDirectlyKey]) { if (k === "clone") throw newBodyUnusableError(); return rejectBodyUnusable(); } return this[getRequestCache]()[k](); } }); }); Object.defineProperty(requestPrototype, "text", { value: function() { return readBodyWithFastPath(this, "text", (buf) => textDecoder.decode(buf)); } }); Object.defineProperty(requestPrototype, "arrayBuffer", { value: function() { return readBodyWithFastPath(this, "arrayBuffer", (buf) => toArrayBuffer(buf)); } }); Object.defineProperty(requestPrototype, "blob", { value: function() { return readBodyWithFastPath(this, "blob", (buf, request) => { const type = contentType(request); return new Response(buf, type ? { headers: { "content-type": type } } : void 0).blob(); }); } }); Object.defineProperty(requestPrototype, "json", { value: function() { if (this[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); return this.text().then(JSON.parse); } }); Object.defineProperty(requestPrototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { return `Request (lightweight) ${inspectFn({ method: this.method, url: this.url, headers: this.headers, nativeRequest: this[requestCache] }, { ...options, depth: depth == null ? null : depth - 1 })}`; } }); Object.setPrototypeOf(requestPrototype, Request$1.prototype); var newRequest = (incoming, defaultHostname) => { const req = Object.create(requestPrototype); req[incomingKey] = incoming; req[methodKey] = normalizeIncomingMethod(incoming.method); const incomingUrl = incoming.url || ""; if (incomingUrl[0] !== "/" && (incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://"))) { if (incoming instanceof node_http2.Http2ServerRequest) throw new RequestError("Absolute URL for :path is not allowed in HTTP/2"); try { req[urlKey] = new URL(incomingUrl).href; } catch (e) { throw new RequestError("Invalid absolute URL", { cause: e }); } return req; } const host = (incoming instanceof node_http2.Http2ServerRequest ? incoming.authority : incoming.headers.host) || defaultHostname; if (!host) throw new RequestError("Missing host header"); let scheme; if (incoming instanceof node_http2.Http2ServerRequest) { scheme = incoming.scheme; if (!(scheme === "http" || scheme === "https")) throw new RequestError("Unsupported scheme"); } else scheme = incoming.socket && incoming.socket.encrypted ? "https" : "http"; try { req[urlKey] = buildUrl(scheme, host, incomingUrl); } catch (e) { if (e instanceof RequestError) throw e; else throw new RequestError("Invalid URL", { cause: e }); } return req; }; var defaultContentType = "text/plain; charset=UTF-8"; var responseCache = Symbol("responseCache"); var getResponseCache = Symbol("getResponseCache"); var cacheKey = Symbol("cache"); var GlobalResponse = global.Response; var Response$1 = class Response$1 { #body; #init; [getResponseCache]() { const cache = this[cacheKey]; const liveHeaders = cache && cache[2] instanceof Headers ? cache[2] : void 0; delete this[cacheKey]; return this[responseCache] ||= new GlobalResponse(this.#body, liveHeaders ? { status: this.#init?.status, statusText: this.#init?.statusText, headers: liveHeaders } : this.#init); } constructor(body, init) { let headers; this.#body = body; if (init instanceof Response$1) { const cachedGlobalResponse = init[responseCache]; if (cachedGlobalResponse) { this.#init = cachedGlobalResponse; this[getResponseCache](); return; } else { this.#init = init.#init; headers = new Headers(init.headers); } } else this.#init = init; if (body == null || typeof body === "string" || typeof body?.getReader !== "undefined" || body instanceof Blob || body instanceof Uint8Array) this[cacheKey] = [ init?.status || 200, body ?? null, headers || init?.headers ]; } get headers() { const cache = this[cacheKey]; if (cache) { if (!(cache[2] instanceof Headers)) cache[2] = new Headers(cache[2] || (cache[1] === null ? void 0 : { "content-type": defaultContentType })); return cache[2]; } return this[getResponseCache]().headers; } get status() { return this[cacheKey]?.[0] ?? this[getResponseCache]().status; } get ok() { const status = this.status; return status >= 200 && status < 300; } }; [ "body", "bodyUsed", "redirected", "statusText", "trailers", "type", "url" ].forEach((k) => { Object.defineProperty(Response$1.prototype, k, { get() { return this[getResponseCache]()[k]; } }); }); [ "arrayBuffer", "blob", "clone", "formData", "json", "text" ].forEach((k) => { Object.defineProperty(Response$1.prototype, k, { value: function() { return this[getResponseCache]()[k](); } }); }); Object.defineProperty(Response$1.prototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { return `Response (lightweight) ${inspectFn({ status: this.status, headers: this.headers, ok: this.ok, nativeResponse: this[responseCache] }, { ...options, depth: depth == null ? null : depth - 1 })}`; } }); Object.setPrototypeOf(Response$1, GlobalResponse); Object.setPrototypeOf(Response$1.prototype, GlobalResponse.prototype); var validRedirectUrl = /^https?:\/\/[!#-;=?-[\]_a-z~A-Z]+$/; var parseRedirectUrl = (url) => { if (url instanceof URL) return url.href; if (validRedirectUrl.test(url)) return url; return new URL(url).href; }; var validRedirectStatuses = new Set([ 301, 302, 303, 307, 308 ]); Object.defineProperty(Response$1, "redirect", { value: function redirect(url, status = 302) { if (!validRedirectStatuses.has(status)) throw new RangeError("Invalid status code"); return new Response$1(null, { status, headers: { location: parseRedirectUrl(url) } }); }, writable: true, configurable: true }); Object.defineProperty(Response$1, "json", { value: function json(data, init) { const body = JSON.stringify(data); if (body === void 0) throw new TypeError("The data is not JSON serializable"); const initHeaders = init?.headers; let headers; if (initHeaders) { headers = new Headers(initHeaders); if (!headers.has("content-type")) headers.set("content-type", "application/json"); } else headers = { "content-type": "application/json" }; return new Response$1(body, { status: init?.status ?? 200, statusText: init?.statusText, headers }); }, writable: true, configurable: true }); async function readWithoutBlocking(readPromise) { return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(void 0))]); } function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromise) { const cancel = (error) => { reader.cancel(error).catch(() => {}); }; writable.on("close", cancel); writable.on("error", cancel); (currentReadPromise ?? reader.read()).then(flow, handleStreamError); return reader.closed.finally(() => { writable.off("close", cancel); writable.off("error", cancel); }); function handleStreamError(error) { if (error) writable.destroy(error); } function onDrain() { reader.read().then(flow, handleStreamError); } function flow({ done, value }) { try { if (done) writable.end(); else if (!writable.write(value)) writable.once("drain", onDrain); else return reader.read().then(flow, handleStreamError); } catch (e) { handleStreamError(e); } } } function writeFromReadableStream(stream, writable) { if (stream.locked) throw new TypeError("ReadableStream is locked."); else if (writable.destroyed) return; return writeFromReadableStreamDefaultReader(stream.getReader(), writable); } var buildOutgoingHttpHeaders = (headers, defaultContentType) => { const res = {}; if (!(headers instanceof Headers)) headers = new Headers(headers ?? void 0); if (headers.has("set-cookie")) { const cookies = []; for (const [k, v] of headers) if (k === "set-cookie") cookies.push(v); else res[k] = v; if (cookies.length > 0) res["set-cookie"] = cookies; } else for (const [k, v] of headers) res[k] = v; if (defaultContentType) res["content-type"] ??= defaultContentType; return res; }; var outgoingEnded = Symbol("outgoingEnded"); var incomingDraining = Symbol("incomingDraining"); var DRAIN_TIMEOUT_MS = 500; var MAX_DRAIN_BYTES = 64 * 1024 * 1024; var drainIncoming = (incoming) => { const incomingWithDrainState = incoming; if (incoming.destroyed || incomingWithDrainState[incomingDraining]) return; incomingWithDrainState[incomingDraining] = true; if (incoming instanceof node_http2.Http2ServerRequest) { try { incoming.stream?.close?.(node_http2.constants.NGHTTP2_NO_ERROR); } catch {} return; } let bytesRead = 0; const cleanup = () => { clearTimeout(timer); incoming.off("data", onData); incoming.off("end", cleanup); incoming.off("error", cleanup); }; const forceClose = () => { cleanup(); const socket = incoming.socket; if (socket && !socket.destroyed) socket.destroySoon(); }; const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS); timer.unref?.(); const onData = (chunk) => { bytesRead += chunk.length; if (bytesRead > MAX_DRAIN_BYTES) forceClose(); }; incoming.on("data", onData); incoming.on("end", cleanup); incoming.on("error", cleanup); incoming.resume(); }; var makeCloseHandler = (req, incoming, outgoing, needsBodyCleanup) => () => { if (incoming.errored) req[abortRequest](incoming.errored.toString()); else if (!outgoing.writableFinished) req[abortRequest]("Client connection prematurely closed."); if (needsBodyCleanup && !incoming.readableEnded) setTimeout(() => { if (!incoming.readableEnded) setTimeout(() => { drainIncoming(incoming); }); }); }; var isImmediateCacheableResponse = (res) => { if (!(cacheKey in res)) return false; const body = res[cacheKey][1]; return body === null || typeof body === "string" || body instanceof Uint8Array; }; var handleRequestError = () => new Response(null, { status: 400 }); var handleFetchError = (e) => new Response(null, { status: e instanceof Error && (e.name === "TimeoutError" || e.constructor.name === "TimeoutError") ? 504 : 500 }); var handleResponseError = (e, outgoing) => { const err = e instanceof Error ? e : new Error("unknown error", { cause: e }); if (err.code === "ERR_STREAM_PREMATURE_CLOSE") console.info("The user aborted a request."); else { console.error(e); if (!outgoing.headersSent) outgoing.writeHead(500, { "Content-Type": "text/plain" }); outgoing.end(`Error: ${err.message}`); outgoing.destroy(err); } }; var flushHeaders = (outgoing) => { if ("flushHeaders" in outgoing && outgoing.writable) outgoing.flushHeaders(); }; var responseViaCache = async (res, outgoing) => { let [status, body, header] = res[cacheKey]; if (!header) { if (body === null) { outgoing.writeHead(status); outgoing.end(); } else if (typeof body === "string") { outgoing.writeHead(status, { "Content-Type": defaultContentType, "Content-Length": Buffer.byteLength(body) }); outgoing.end(body); } else if (body instanceof Uint8Array) { outgoing.writeHead(status, { "Content-Type": defaultContentType, "Content-Length": body.byteLength }); outgoing.end(body); } else if (body instanceof Blob) { outgoing.writeHead(status, { "Content-Type": defaultContentType, "Content-Length": body.size }); outgoing.end(new Uint8Array(await body.arrayBuffer())); } else { outgoing.writeHead(status, { "Content-Type": defaultContentType }); flushHeaders(outgoing); await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); } outgoing[outgoingEnded]?.(); return; } let hasContentLength = false; if (header instanceof Headers) { hasContentLength = header.has("content-length"); header = buildOutgoingHttpHeaders(header, body === null ? void 0 : defaultContentType); } else if (Array.isArray(header)) { const headerObj = new Headers(header); hasContentLength = headerObj.has("content-length"); header = buildOutgoingHttpHeaders(headerObj, body === null ? void 0 : defaultContentType); } else for (const key in header) if (key.length === 14 && key.toLowerCase() === "content-length") { hasContentLength = true; break; } if (!hasContentLength) { if (typeof body === "string") header["Content-Length"] = Buffer.byteLength(body); else if (body instanceof Uint8Array) header["Content-Length"] = body.byteLength; else if (body instanceof Blob) header["Content-Length"] = body.size; } outgoing.writeHead(status, header); if (body == null) outgoing.end(); else if (typeof body === "string" || body instanceof Uint8Array) outgoing.end(body); else if (body instanceof Blob) outgoing.end(new Uint8Array(await body.arrayBuffer())); else { flushHeaders(outgoing); await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); } outgoing[outgoingEnded]?.(); }; var isPromise = (res) => typeof res.then === "function"; var responseViaResponseObject = async (res, outgoing, options = {}) => { if (isPromise(res)) if (options.errorHandler) try { res = await res; } catch (err) { const errRes = await options.errorHandler(err); if (!errRes) return; res = errRes; } else res = await res.catch(handleFetchError); if (cacheKey in res) return responseViaCache(res, outgoing); const resHeaderRecord = buildOutgoingHttpHeaders(res.headers, res.body === null ? void 0 : defaultContentType); if (res.body) { const reader = res.body.getReader(); const values = []; let done = false; let currentReadPromise = void 0; if (resHeaderRecord["transfer-encoding"] !== "chunked") { let maxReadCount = 2; for (let i = 0; i < maxReadCount; i++) { currentReadPromise ||= reader.read(); const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => { console.error(e); done = true; }); if (!chunk) { if (i === 1) { await new Promise((resolve) => setTimeout(resolve)); maxReadCount = 3; continue; } break; } currentReadPromise = void 0; if (chunk.value) values.push(chunk.value); if (chunk.done) { done = true; break; } } if (done && !("content-length" in resHeaderRecord)) resHeaderRecord["content-length"] = values.reduce((acc, value) => acc + value.length, 0); } outgoing.writeHead(res.status, resHeaderRecord); values.forEach((value) => { outgoing.write(value); }); if (done) outgoing.end(); else { if (values.length === 0) flushHeaders(outgoing); await writeFromReadableStreamDefaultReader(reader, outgoing, currentReadPromise); } } else if (resHeaderRecord[require_constants.X_ALREADY_SENT]) {} else { outgoing.writeHead(res.status, resHeaderRecord); outgoing.end(); } outgoing[outgoingEnded]?.(); }; var getRequestListener = (fetchCallback, options = {}) => { const autoCleanupIncoming = options.autoCleanupIncoming ?? true; if (options.overrideGlobalObjects !== false && global.Request !== Request$1) { Object.defineProperty(global, "Request", { value: Request$1 }); Object.defineProperty(global, "Response", { value: Response$1 }); } return async (incoming, outgoing) => { let res, req; let needsBodyCleanup = false; let closeHandlerAttached = false; const ensureCloseHandler = () => { if (!req || closeHandlerAttached) return; closeHandlerAttached = true; outgoing.on("close", makeCloseHandler(req, incoming, outgoing, needsBodyCleanup)); }; try { req = newRequest(incoming, options.hostname); needsBodyCleanup = autoCleanupIncoming && !(incoming.method === "GET" || incoming.method === "HEAD"); if (needsBodyCleanup) { incoming[wrapBodyStream] = true; if (incoming instanceof node_http2.Http2ServerRequest) outgoing[outgoingEnded] = () => { if (!incoming.readableEnded) setTimeout(() => { if (!incoming.readableEnded) setTimeout(() => { incoming.destroy(); outgoing.destroy(); }); }); }; } res = fetchCallback(req, { incoming, outgoing }); if (!isPromise(res) && isImmediateCacheableResponse(res)) { if (needsBodyCleanup && !incoming.readableEnded) outgoing.once("finish", () => { if (!incoming.readableEnded) drainIncoming(incoming); }); return responseViaCache(res, outgoing); } ensureCloseHandler(); } catch (e) { if (!res) if (options.errorHandler) { ensureCloseHandler(); res = await options.errorHandler(req ? e : toRequestError(e)); if (!res) return; } else if (!req) res = handleRequestError(); else res = handleFetchError(e); else return handleResponseError(e, outgoing); } try { return await responseViaResponseObject(res, outgoing, options); } catch (e) { return handleResponseError(e, outgoing); } }; }; /** * @link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent */ var CloseEvent = globalThis.CloseEvent ?? class extends Event { #eventInitDict; constructor(type, eventInitDict = {}) { super(type, eventInitDict); this.#eventInitDict = eventInitDict; } get wasClean() { return this.#eventInitDict.wasClean ?? false; } get code() { return this.#eventInitDict.code ?? 0; } get reason() { return this.#eventInitDict.reason ?? ""; } }; var generateConnectionSymbol = () => Symbol("connection"); var CONNECTION_SYMBOL_KEY = Symbol("CONNECTION_SYMBOL_KEY"); var WAIT_FOR_WEBSOCKET_SYMBOL = Symbol("WAIT_FOR_WEBSOCKET_SYMBOL"); var responseHeadersToSkip = new Set([ "connection", "content-length", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", "sec-websocket-accept", "sec-websocket-extensions", "sec-websocket-protocol" ]); var appendResponseHeaders = (headers, responseHeaders) => { if (!responseHeaders) return; responseHeaders.forEach((value, key) => { if (responseHeadersToSkip.has(key.toLowerCase())) return; headers.push(`${key}: ${value}`); }); }; var rejectUpgradeRequest = (socket, status, responseHeaders) => { const responseLines = ["Connection: close", "Content-Length: 0"]; appendResponseHeaders(responseLines, responseHeaders); socket.end(`HTTP/1.1 ${status.toString()} ${node_http.STATUS_CODES[status] ?? ""}\r\n${responseLines.join("\r\n")}\r\n\r `); }; var createUpgradeRequest = (request) => { const protocol = request.socket.encrypted ? "https" : "http"; const url = new URL(request.url ?? "/", `${protocol}://${request.headers.host ?? "localhost"}`); const headers = new Headers(); for (const key in request.headers) { const value = request.headers[key]; if (!value) continue; headers.append(key, Array.isArray(value) ? value[0] : value); } return new Request(url, { headers }); }; var setupWebSocket = (options) => { const { server, fetchCallback, wss } = options; const waiterMap = /* @__PURE__ */ new Map(); wss.on("connection", (ws, request) => { const waiter = waiterMap.get(request); if (waiter) { waiter.resolve(ws); waiterMap.delete(request); } }); const waitForWebSocket = (request, connectionSymbol) => { return new Promise((resolve) => { waiterMap.set(request, { resolve, connectionSymbol }); }); }; server.on("upgrade", async (request, socket, head) => { if (request.headers.upgrade?.toLowerCase() !== "websocket") return; const env = { incoming: request, outgoing: void 0, wss, [WAIT_FOR_WEBSOCKET_SYMBOL]: waitForWebSocket }; let status = 400; let responseHeaders; try { const response = await fetchCallback(createUpgradeRequest(request), env); if (response instanceof Response) { status = response.status; responseHeaders = response.headers; } } catch { if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, 500); return; } const waiter = waiterMap.get(request); if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) { waiterMap.delete(request); if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, status, responseHeaders); return; } const addResponseHeaders = (headers) => { appendResponseHeaders(headers, responseHeaders); }; wss.on("headers", addResponseHeaders); try { wss.handleUpgrade(request, socket, head, (ws) => { wss.emit("connection", ws, request); }); } finally { wss.off("headers", addResponseHeaders); } }); server.on("close", () => { wss.close(); }); }; var upgradeWebSocket = (0, hono_ws.defineWebSocketHelper)(async (c, events, options) => { if (c.req.header("upgrade")?.toLowerCase() !== "websocket") return; const env = c.env; const waitForWebSocket = env[WAIT_FOR_WEBSOCKET_SYMBOL]; if (!waitForWebSocket || !env.incoming) return new Response(null, { status: 500 }); const connectionSymbol = generateConnectionSymbol(); env[CONNECTION_SYMBOL_KEY] = connectionSymbol; (async () => { const ws = await waitForWebSocket(env.incoming, connectionSymbol); const messagesReceivedInStarting = []; const bufferMessage = (data, isBinary) => { messagesReceivedInStarting.push([data, isBinary]); }; ws.on("message", bufferMessage); const ctx = { binaryType: "arraybuffer", close(code, reason) { ws.close(code, reason); }, protocol: ws.protocol, raw: ws, get readyState() { return ws.readyState; }, send(source, opts) { ws.send(source, { compress: opts?.compress }); }, url: new URL(c.req.url) }; try { events?.onOpen?.(new Event("open"), ctx); } catch (e) { (options?.onError ?? console.error)(e); } const handleMessage = (data, isBinary) => { const datas = Array.isArray(data) ? data : [data]; for (const data of datas) try { events?.onMessage?.(new MessageEvent("message", { data: isBinary ? data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : typeof data === "string" ? data : Buffer.from(data).toString("utf-8") }), ctx); } catch (e) { (options?.onError ?? console.error)(e); } }; ws.off("message", bufferMessage); for (const message of messagesReceivedInStarting) handleMessage(...message); ws.on("message", (data, isBinary) => { handleMessage(data, isBinary); }); ws.on("close", (code, reason) => { try { events?.onClose?.(new CloseEvent("close", { code, reason: reason.toString() }), ctx); } catch (e) { (options?.onError ?? console.error)(e); } }); ws.on("error", (error) => { try { events?.onError?.(new ErrorEvent("error", { error }), ctx); } catch (e) { (options?.onError ?? console.error)(e); } }); })(); return new Response(); }); var createAdaptorServer = (options) => { const fetchCallback = options.fetch; const requestListener = getRequestListener(fetchCallback, { hostname: options.hostname, overrideGlobalObjects: options.overrideGlobalObjects, autoCleanupIncoming: options.autoCleanupIncoming }); const server = (options.createServer || node_http.createServer)(options.serverOptions || {}, requestListener); if (options.websocket && options.websocket.server) { if (options.websocket.server.options.noServer !== true) throw new Error("WebSocket server must be created with { noServer: true } option"); setupWebSocket({ server, fetchCallback, wss: options.websocket.server }); } return server; }; var serve = (options, listeningListener) => { const server = createAdaptorServer(options); server.listen(options?.port ?? 3e3, options.hostname, () => { const serverInfo = server.address(); listeningListener && listeningListener(serverInfo); }); return server; }; exports.RequestError = RequestError; exports.createAdaptorServer = createAdaptorServer; exports.getRequestListener = getRequestListener; exports.serve = serve; exports.upgradeWebSocket = upgradeWebSocket; })))(); var github_exports = /* @__PURE__ */ __exportAll({ channel: () => channel, client: () => client, commentOnIssue: () => commentOnIssue, getPullRequestDiff: () => getPullRequestDiff }); /** * GitHub channel — the webhook (server) deployment mode. Lets people summon * Shippie by commenting `/shippie ...` on an issue or pull request. Verified * deliveries are dispatched to the `mention` agent, which replies via Octokit. * * Served at `POST /channels/github/webhook` on the built server. Requires * GITHUB_WEBHOOK_SECRET (verify inbound) and GITHUB_TOKEN (outbound comments). * This is separate from the one-shot CI review (action.yml / `flue run review`). */ var MENTION = "/shippie"; var client = new Octokit({ auth: process.env.GITHUB_TOKEN }); var channel = createGitHubChannel({ webhookSecret: process.env.GITHUB_WEBHOOK_SECRET || `shippie-unconfigured-${randomUUID()}`, async webhook({ delivery }) { if (delivery.name === "issue_comment" && delivery.payload.action === "created") { const { repository, issue, comment, sender } = delivery.payload; if (sender?.type === "Bot") return void 0; if (!comment.body?.toLowerCase().includes(MENTION)) return void 0; const ref = { owner: repository.owner.login, repo: repository.name, issueNumber: issue.number }; await dispatch(mention_default, { id: channel.conversationKey(ref), input: { type: "github.mention", isPullRequest: Boolean(issue.pull_request), title: issue.title, author: sender?.login, request: comment.body } }); return; } if (delivery.name === "pull_request_review_comment" && delivery.payload.action === "created") { const { repository, pull_request, comment, sender } = delivery.payload; if (sender?.type === "Bot") return void 0; if (!comment.body?.toLowerCase().includes(MENTION)) return void 0; const ref = { owner: repository.owner.login, repo: repository.name, issueNumber: pull_request.number }; await dispatch(mention_default, { id: channel.conversationKey(ref), input: { type: "github.mention", isPullRequest: true, title: pull_request.title, author: sender?.login, request: comment.body, path: comment.path, line: comment.line ?? null } }); return; } } }); /** Tool: post a reply comment on the issue/PR that summoned Shippie. */ var commentOnIssue = (ref) => defineTool({ name: "comment_on_github_issue", description: "Post your reply as a comment on the GitHub issue or pull request that mentioned you.", parameters: v.object({ body: v.pipe(v.string(), v.minLength(1), v.description("The markdown comment to post.")) }), async execute({ body }) { return `Comment posted: ${(await client.rest.issues.createComment({ owner: ref.owner, repo: ref.repo, issue_number: ref.issueNumber, body })).data.html_url}`; } }); /** Tool: fetch the unified diff of the pull request that summoned Shippie. */ var getPullRequestDiff = (ref) => defineTool({ name: "get_pull_request_diff", description: "Fetch the unified diff of the pull request that mentioned you. Call this before reviewing a PR.", parameters: v.object({}), async execute() { const res = await client.rest.pulls.get({ owner: ref.owner, repo: ref.repo, pull_number: ref.issueNumber, mediaType: { format: "diff" } }); return typeof res.data === "string" ? res.data : JSON.stringify(res.data); } }); //#endregion //#region src/agents/mention.ts var mention_exports = /* @__PURE__ */ __exportAll({ default: () => mention_default }); /** * The `/shippie` mention agent (webhook/channel mode). Dispatched by the GitHub * channel when someone comments `/shippie ...` on an issue or PR. It reads the * request, optionally fetches the PR diff, and replies with a single comment. * * Runs on a deployed Flue server (not a repo checkout), so it works through the * GitHub API tools rather than a `local()` sandbox. */ var mention_default = createAgent(({ id }) => { const ref = channel.parseConversationKey(id); return { model: process.env.SHIPPIE_MODEL ?? "anthropic/claude-sonnet-4-6", instructions: `You are Shippie, an automated code-review agent summoned by a "/shippie" command on ${ref.owner}/${ref.repo} #${ref.issueNumber}. The incoming message describes the user's request. Decide what they want: - If it is a pull request and they ask you to review it (e.g. "/shippie review"), call get_pull_request_diff, review the changed code for bugs, exposed secrets, missing tests, and risky changes, then write a concise review. - For any other question, answer it helpfully and concisely based on the request and what you can fetch. Rules: - Be brief and specific. Do not restate the whole diff back to the user. - Only raise issues you are confident about. - ALWAYS finish by calling comment_on_github_issue exactly once with your reply (markdown).`, tools: [commentOnIssue(ref), getPullRequestDiff(ref)] }; }); //#endregion //#region src/common/formatting/summary.ts /** * Constants for formatting comments */ var FORMATTING = { SUMMARY_TITLE: "## General Summary šŸ“ā€ā˜ ļø", SEPARATOR: "\n\n---\n\n", SIGN_OFF: "### Review powered by [Shippie 🚢](https://github.com/mattzcarey/shippie)", CTA: `<details> <summary>šŸš€ Good review?</summary> --- **Help us improve!** Your feedback and support make Shippie better for everyone. ⭐ **Quick win?** [Star the repo](https://github.com/mattzcarey/shippie) if you find it useful šŸ’” **Have ideas?** [Open a discussion](https://github.com/mattzcarey/shippie/discussions) šŸ› ļø **Wanna chat about agents?** [Send me a DM](https://x.com/mattzcarey) --- *Sponsor the project* to preview features and influence the roadmap šŸ‘‰ [YOUR COMPANY HERE](https://sustain.dev/sponsor/shippie) šŸ‘ˆ </details>`, TOOL_CALLS_TITLE: "šŸ› ļø Tool Calls", TOKEN_USAGE_TITLE: "šŸ“Š Token Usage" }; /** * Formats a thread comment with title, content, and sign-off */ var formatSummary = (comment) => { return `${FORMATTING.SUMMARY_TITLE}\n\n${comment}${FORMATTING.SEPARATOR}${FORMATTING.SIGN_OFF}\n\n${FORMATTING.CTA}`; }; //#endregion //#region src/github/reporter.ts /** Make a workspace-absolute path relative to the repo root for the GitHub API. */ var toRepoPath = (workspace, filePath) => isAbsolute(filePath) ? relative(workspace, filePath) : filePath; var createGithubReporter = (cfg) => { const target = cfg.github; if (!target) throw new Error("GitHub reporter requires owner/repo/prNumber. Is this running on a PR?"); const octokit = new Octokit({ auth: target.token }); const { owner, repo, prNumber } = target; const resolveCommitId = async () => { if (cfg.headSha) return cfg.headSha; return (await octokit.rest.pulls.get({ owner, repo, pull_number: prNumber })).data.head.sha; }; return { postReviewComment: async ({ filePath, comment, startLine, endLine }) => { const path = toRepoPath(cfg.workspace, filePath); const commit_id = await resolveCommitId(); const line = endLine ?? startLine; try { const multiLine = startLine && endLine && startLine !== endLine; const { data } = await octokit.rest.pulls.createReviewComment({ owner, repo, pull_number: prNumber, commit_id, body: comment, path, line, ...multiLine ? { start_line: startLine, start_side: "RIGHT", side: "RIGHT" } : {} }); return data.html_url; } catch (error) { throw new Error(`Failed to post review comment on ${path}: ${error instanceof Error ? error.message : String(error)}`); } }, postSummary: async (comment) => { const body = formatSummary(comment); const { data: existing } = await octokit.rest.issues.listComments({ owner, repo, issue_number: prNumber }); const prior = existing.find((c) => c.body?.includes(FORMATTING.SIGN_OFF)); if (prior) { const { data } = await octokit.rest.issues.updateComment({ owner, repo, comment_id: prior.id, body }); return data.html_url; } const { data } = await octokit.rest.issues.createComment({ owner, repo, issue_number: prNumber, body }); return data.html_url; } }; }; var LOCAL_RUN_TIMESTAMP = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-"); var createLocalReporter = (cfg) => { const reviewDir = join(cfg.workspace, ".shippie", "review"); const reviewFile = join(reviewDir, `local_${LOCAL_RUN_TIMESTAMP}.md`); const ensureDir = async () => { await mkdir(reviewDir, { recursive: true }); await writeFile(join(reviewDir, ".gitignore"), "*").catch(() => {}); }; return { postReviewComment: async ({ filePath, comment, startLine, endLine }) => { await ensureDir(); await appendFile(reviewFile, `### ${toRepoPath(cfg.workspace, filePath)}${startLine ? `:${startLine}${endLine && endLine !== startLine ? `-${endLine}` : ""}` : ""}\n\n${comment}\n\n`); return `Comment written to ${reviewFile}`; }, postSummary: async (comment) => { await ensureDir(); await appendFile(reviewFile, `${formatSummary(comment)}\n`); return `Summary written to ${reviewFile}`; } }; }; var createReporter = (cfg) => { if (cfg.platform === "github" && !cfg.github) { console.error("[shippie] platform is \"github\" but no PR context was found (owner/repo/prNumber). Falling back to local file output. Set GITHUB_TOKEN + PR metadata to post on the PR."); return createLocalReporter(cfg); } return cfg.platform === "github" ? createGithubReporter(cfg) : createLocalReporter(cfg); }; //#endregion //#region src/review/config.ts var DEFAULT_MODEL = "anthropic/claude-sonnet-4-6"; var DEFAULT_THINKING = "medium"; var parseMcpServers = (payload, env) => { if (payload.mcpServers && Object.keys(payload.mcpServers).length