@papi-ai/server
Advanced tools
| // src/cli/statusline.ts | ||
| import { createHash } from "crypto"; | ||
| import { existsSync, readFileSync, writeFileSync } from "fs"; | ||
| import { homedir, tmpdir } from "os"; | ||
| import { join } from "path"; | ||
| import { pathToFileURL } from "url"; | ||
| var CACHE_TTL_MS = 3e4; | ||
| var FETCH_TIMEOUT_MS = 3e3; | ||
| function renderPeekLine(c) { | ||
| const parts = [`PAPI c${c.cycle ?? "?"}`]; | ||
| parts.push(`build ${c.inFlight}`); | ||
| parts.push(`review ${c.inReview}`); | ||
| if (c.ownerActions !== null) parts.push(`you ${c.ownerActions}`); | ||
| return parts.join(" | "); | ||
| } | ||
| function cacheFilePath(endpoint, projectId) { | ||
| const hash = createHash("sha256").update(`${endpoint}|${projectId ?? ""}`).digest("hex").slice(0, 16); | ||
| return join(tmpdir(), `papi-statusline-${hash}.json`); | ||
| } | ||
| function readClientCache(path, now = Date.now()) { | ||
| try { | ||
| const hit = JSON.parse(readFileSync(path, "utf-8")); | ||
| if (!hit || typeof hit.expires !== "number" || hit.expires <= now) return void 0; | ||
| return hit.payload; | ||
| } catch { | ||
| return void 0; | ||
| } | ||
| } | ||
| function writeClientCache(path, payload, now = Date.now()) { | ||
| try { | ||
| writeFileSync(path, JSON.stringify({ expires: now + CACHE_TTL_MS, payload })); | ||
| } catch { | ||
| } | ||
| } | ||
| function configFromMcpJson(body) { | ||
| if (!body || typeof body !== "object") return void 0; | ||
| const servers = body.mcpServers; | ||
| if (!servers || typeof servers !== "object") return void 0; | ||
| for (const entry of Object.values(servers)) { | ||
| if (!entry || typeof entry !== "object") continue; | ||
| const s = entry; | ||
| if (typeof s.url !== "string" || !/papi/i.test(s.url)) continue; | ||
| const auth = s.headers?.["Authorization"]; | ||
| const authString = typeof auth === "string" ? auth : ""; | ||
| const bearer = authString.startsWith("Bearer ") ? authString.slice(7).trim() : ""; | ||
| const projectHeader = s.headers?.["x-papi-project-id"]; | ||
| if (!bearer) continue; | ||
| return { | ||
| endpoint: new URL(s.url).origin, | ||
| bearer, | ||
| projectId: typeof projectHeader === "string" && projectHeader.length > 0 ? projectHeader : void 0 | ||
| }; | ||
| } | ||
| return void 0; | ||
| } | ||
| function mergeStatusLine(body, command) { | ||
| const obj = body && typeof body === "object" ? body : {}; | ||
| if (obj["statusLine"] !== void 0) return null; | ||
| return { ...obj, statusLine: { type: "command", command } }; | ||
| } | ||
| function resolveConfig(argv) { | ||
| const flag = (name) => { | ||
| const i = argv.indexOf(name); | ||
| return i >= 0 && argv[i + 1] ? argv[i + 1] : void 0; | ||
| }; | ||
| const endpoint = flag("--endpoint") ?? process.env["PAPI_ENDPOINT"]; | ||
| const bearer = flag("--bearer") ?? process.env["PAPI_BEARER"]; | ||
| const projectId = flag("--project") ?? process.env["PAPI_PROJECT_ID"]; | ||
| if (endpoint && bearer) return { endpoint: new URL(endpoint).origin, bearer, projectId }; | ||
| const mcpJsonPath = join(process.cwd(), ".mcp.json"); | ||
| if (existsSync(mcpJsonPath)) { | ||
| try { | ||
| return configFromMcpJson(JSON.parse(readFileSync(mcpJsonPath, "utf-8"))); | ||
| } catch { | ||
| return void 0; | ||
| } | ||
| } | ||
| return void 0; | ||
| } | ||
| async function fetchPeek(config) { | ||
| const response = await fetch(`${config.endpoint}/peek`, { | ||
| headers: { | ||
| Authorization: `Bearer ${config.bearer}`, | ||
| ...config.projectId ? { "x-papi-project-id": config.projectId } : {} | ||
| }, | ||
| signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) | ||
| }); | ||
| if (!response.ok) throw new Error(`peek returned ${response.status}`); | ||
| const body = await response.json(); | ||
| return { | ||
| cycle: typeof body.cycle === "number" ? body.cycle : null, | ||
| inFlight: typeof body.inFlight === "number" ? body.inFlight : 0, | ||
| inReview: typeof body.inReview === "number" ? body.inReview : 0, | ||
| ownerActions: typeof body.ownerActions === "number" ? body.ownerActions : null | ||
| }; | ||
| } | ||
| var RENDER_COMMAND = "npx -y @papi-ai/cli papi-statusline"; | ||
| async function install() { | ||
| const settingsPath = join(homedir(), ".claude", "settings.json"); | ||
| let body = {}; | ||
| if (existsSync(settingsPath)) { | ||
| try { | ||
| body = JSON.parse(readFileSync(settingsPath, "utf-8")); | ||
| } catch (err) { | ||
| console.error(`papi-statusline: could not parse ${settingsPath}: ${err.message}`); | ||
| return 1; | ||
| } | ||
| } | ||
| const merged = mergeStatusLine(body, RENDER_COMMAND); | ||
| if (merged === null) { | ||
| console.log( | ||
| `papi-statusline: ${settingsPath} already has a statusLine entry \u2014 left untouched. | ||
| To switch it to PAPI manually, set: | ||
| "statusLine": { "type": "command", "command": "${RENDER_COMMAND}" } | ||
| Any other MCP client with a custom status-bar command can run the same command.` | ||
| ); | ||
| return 0; | ||
| } | ||
| writeFileSync(settingsPath, `${JSON.stringify(merged, null, 2)} | ||
| `); | ||
| console.log(`papi-statusline: installed into ${settingsPath}. Restart your AI client to see it.`); | ||
| return 0; | ||
| } | ||
| async function runStatusline(argv, stdin) { | ||
| if (argv.includes("--install")) return install(); | ||
| void stdin; | ||
| const config = resolveConfig(argv); | ||
| if (!config) return 0; | ||
| const cachePath = cacheFilePath(config.endpoint, config.projectId); | ||
| const cached = readClientCache(cachePath); | ||
| if (cached) { | ||
| process.stdout.write(`${renderPeekLine(cached)} | ||
| `); | ||
| return 0; | ||
| } | ||
| try { | ||
| const payload = await fetchPeek(config); | ||
| writeClientCache(cachePath, payload); | ||
| process.stdout.write(`${renderPeekLine(payload)} | ||
| `); | ||
| } catch { | ||
| } | ||
| return 0; | ||
| } | ||
| if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { | ||
| const chunks = []; | ||
| process.stdin.on("data", (chunk) => chunks.push(chunk)); | ||
| process.stdin.on("end", () => { | ||
| void runStatusline(process.argv.slice(2), Buffer.concat(chunks).toString("utf-8")).then((code) => { | ||
| process.exitCode = code; | ||
| }); | ||
| }); | ||
| setTimeout(() => { | ||
| void runStatusline(process.argv.slice(2), Buffer.concat(chunks).toString("utf-8")).then((code) => { | ||
| process.exitCode = code; | ||
| }); | ||
| }, 250); | ||
| } | ||
| export { | ||
| cacheFilePath, | ||
| configFromMcpJson, | ||
| mergeStatusLine, | ||
| readClientCache, | ||
| renderPeekLine, | ||
| runStatusline, | ||
| writeClientCache | ||
| }; |
+5
-6
| { | ||
| "name": "@papi-ai/server", | ||
| "version": "0.7.85", | ||
| "description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects", | ||
| "version": "0.7.98", | ||
| "description": "PAPI MCP server \u2014 AI-powered sprint planning, build execution, and strategy review for software projects", | ||
| "license": "Elastic-2.0", | ||
@@ -52,13 +52,12 @@ "mcpName": "io.github.getpapi/papi", | ||
| "dependencies": { | ||
| "@anthropic-ai/sdk": "^0.82.0", | ||
| "@anthropic-ai/sdk": "^0.91.1", | ||
| "@modelcontextprotocol/sdk": "^1.27.1", | ||
| "@papi-ai/adapter-pg": "^0.2.16", | ||
| "@papi-ai/shared": "^0.2.5", | ||
| "@papi-ai/skills": "^0.1.4", | ||
| "js-yaml": "^4.1.0" | ||
| "@papi-ai/skills": "^0.1.4" | ||
| }, | ||
| "devDependencies": { | ||
| "@papi-ai/adapter-md": "^0.2.0", | ||
| "@types/js-yaml": "^4.0.9", | ||
| "@types/node": "^22.0.0", | ||
| "ajv": "^8.20.0", | ||
| "tsup": "^8.0.0", | ||
@@ -65,0 +64,0 @@ "typescript": "^5.5.0", |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1987781
5.06%5
-16.67%21
5%40858
5.42%63
5%11
10%+ Added
- Removed
- Removed
- Removed
- Removed
Updated