@papi-ai/server 0.7.7 → 0.7.8

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 +208 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7703,7 +7703,7 @@ ${r.content}` + (r.carry_forward ? `
7703
7703
  FROM cycle_tasks
7704
7704
  WHERE project_id = ${this.projectId}
7705
7705
  AND status NOT IN ('Done', 'Cancelled', 'Archived', 'Deferred')
7706
- AND (task_type IN ('task', 'bug', 'research') OR task_type IS NULL)
7706
+ AND (task_type IN ('task', 'bug', 'research', 'discovery', 'spike') OR task_type IS NULL)
7707
7707
  ORDER BY
7708
7708
  CASE priority
7709
7709
  WHEN 'P0 Critical' THEN 0
@@ -9224,9 +9224,11 @@ var init_git = __esm({
9224
9224
 
9225
9225
  // src/index.ts
9226
9226
  import { readFileSync as readFileSync4 } from "fs";
9227
+ import { createServer as createHttpServer } from "http";
9227
9228
  import { dirname as dirname2, join as join11 } from "path";
9228
9229
  import { fileURLToPath as fileURLToPath2 } from "url";
9229
9230
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9231
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9230
9232
  import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
9231
9233
  import {
9232
9234
  CallToolRequestSchema as CallToolRequestSchema2,
@@ -9249,6 +9251,8 @@ function loadConfig() {
9249
9251
  const baseBranch = process.env.PAPI_BASE_BRANCH ?? "main";
9250
9252
  const autoPR = process.env.PAPI_AUTO_PR !== "false";
9251
9253
  const lightMode = process.env.PAPI_LIGHT_MODE === "true";
9254
+ const projectOwner = process.env.PAPI_OWNER ?? "Cathal";
9255
+ const skipProjectSpecificRules = process.env.PAPI_SKIP_PROJECT_RULES === "true";
9252
9256
  const papiEndpoint = process.env.PAPI_ENDPOINT;
9253
9257
  const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
9254
9258
  const databaseUrl = process.env.DATABASE_URL;
@@ -9263,7 +9267,9 @@ function loadConfig() {
9263
9267
  autoPR,
9264
9268
  adapterType,
9265
9269
  papiEndpoint,
9266
- lightMode
9270
+ lightMode,
9271
+ projectOwner,
9272
+ skipProjectSpecificRules
9267
9273
  };
9268
9274
  }
9269
9275
 
@@ -12083,10 +12089,11 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
12083
12089
  timings["getPlanContextSummary"] = t();
12084
12090
  if (leanSummary) {
12085
12091
  t = startTimer();
12086
- let [metricsSnapshots2, reviews2] = await Promise.all([
12092
+ const [metricsSnapshotsRaw, reviews2] = await Promise.all([
12087
12093
  adapter2.readCycleMetrics(),
12088
12094
  adapter2.getRecentReviews(5)
12089
12095
  ]);
12096
+ let metricsSnapshots2 = metricsSnapshotsRaw;
12090
12097
  let leanBuildReports = [];
12091
12098
  try {
12092
12099
  leanBuildReports = await adapter2.getRecentBuildReports(50);
@@ -17205,7 +17212,7 @@ function extractDocMeta(absolutePath, relativePath, cycleNumber) {
17205
17212
  let title = relativePath.split("/").pop()?.replace(".md", "") ?? relativePath;
17206
17213
  let type = "reference";
17207
17214
  let cycle = cycleNumber;
17208
- let summary = "Auto-registered \u2014 no summary available. Update via doc_register.";
17215
+ const summary = "Auto-registered \u2014 no summary available. Update via doc_register.";
17209
17216
  if (relativePath.startsWith("docs/research/")) type = "research";
17210
17217
  else if (relativePath.startsWith("docs/architecture/")) type = "architecture";
17211
17218
  else if (relativePath.startsWith("docs/audits/")) type = "audit";
@@ -17612,8 +17619,6 @@ For M/L tasks: use the full toolchain \u2014 Playground (design preview) \u2192
17612
17619
  - Check adapter-pg implementation, not adapter-md. adapter-md is legacy.
17613
17620
  - Verify the full write\u2192DB\u2192read\u2192consumer path for any data changes.
17614
17621
  - Run migrations on dev before prod. Test with \`execute_sql\` via Supabase MCP.
17615
- - When adding adapter interface methods, implement in BOTH adapter-md and adapter-pg.
17616
- - Build order matters: adapter-md \u2192 adapter-pg \u2192 server.
17617
17622
  - .papi/ files may be stale \u2014 DB via MCP tools is the source of truth.`,
17618
17623
  Auth: `**MODULE INSTRUCTIONS \u2014 Auth**
17619
17624
  - NEVER expose the Supabase service role key in client-side code or API responses.
@@ -18366,15 +18371,15 @@ ${lines.join("\n")}
18366
18371
  ]);
18367
18372
  warnIfEmpty("getCycleHealth (idea)", health);
18368
18373
  const phase = input.phase || resolveCurrentPhase(phases);
18369
- const VALID_PRIORITIES2 = /* @__PURE__ */ new Set(["P0 Critical", "P1 High", "P2 Medium", "P3 Low"]);
18374
+ const VALID_PRIORITIES3 = /* @__PURE__ */ new Set(["P0 Critical", "P1 High", "P2 Medium", "P3 Low"]);
18370
18375
  const VALID_COMPLEXITIES2 = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
18371
- const priority = input.priority && VALID_PRIORITIES2.has(input.priority) ? input.priority : "P2 Medium";
18376
+ const priority = input.priority && VALID_PRIORITIES3.has(input.priority) ? input.priority : "P2 Medium";
18372
18377
  const complexity = input.complexity && VALID_COMPLEXITIES2.has(input.complexity) ? input.complexity : "Small";
18373
- const VALID_TYPES = /* @__PURE__ */ new Set(["task", "bug", "research", "idea", "spike", "discovery"]);
18378
+ const VALID_TYPES2 = /* @__PURE__ */ new Set(["task", "bug", "research", "idea", "spike", "discovery"]);
18374
18379
  let taskTitle = input.text;
18375
18380
  let taskType = "idea";
18376
18381
  let typeInferred = false;
18377
- if (input.type && VALID_TYPES.has(input.type)) {
18382
+ if (input.type && VALID_TYPES2.has(input.type)) {
18378
18383
  taskType = input.type;
18379
18384
  } else {
18380
18385
  const PREFIX_MAP = {
@@ -18843,16 +18848,16 @@ async function recordAdHoc(adapter2, input) {
18843
18848
  displayId: "",
18844
18849
  title: input.title,
18845
18850
  status: "Done",
18846
- priority: "P3 Low",
18851
+ priority: input.priority || "P2 Medium",
18847
18852
  complexity: input.effort === "XS" || input.effort === "S" ? "Small" : "Medium",
18848
18853
  module: input.module || "Core",
18849
18854
  epic: input.epic || "Platform",
18850
18855
  phase,
18851
- owner: "Cathal",
18856
+ owner: input.owner || "Cathal",
18852
18857
  reviewed: true,
18853
18858
  createdCycle: cycle,
18854
18859
  notes: input.notes ? `[ad-hoc] ${input.notes}` : "[ad-hoc]",
18855
- taskType: "task",
18860
+ taskType: input.taskType || "task",
18856
18861
  source: "owner"
18857
18862
  });
18858
18863
  }
@@ -18877,6 +18882,16 @@ async function recordAdHoc(adapter2, input) {
18877
18882
 
18878
18883
  // src/tools/ad-hoc.ts
18879
18884
  var VALID_EFFORTS = ["XS", "S", "M", "L", "XL"];
18885
+ var VALID_PRIORITIES = ["P0 Critical", "P1 High", "P2 Medium", "P3 Low"];
18886
+ var VALID_TYPES = ["task", "bug", "research", "discovery", "spike", "idea"];
18887
+ function inferTaskType(description) {
18888
+ const lower = description.toLowerCase();
18889
+ if (/\bfix\b|bug|error|crash|broken|regression|defect/.test(lower)) return "bug";
18890
+ if (/research|investigate|explore|analysis|audit|review|assess/.test(lower)) return "research";
18891
+ if (/discover|discovery|found|uncovered/.test(lower)) return "discovery";
18892
+ if (/spike|poc|proof.of.concept|prototype|experiment/.test(lower)) return "spike";
18893
+ return "task";
18894
+ }
18880
18895
  var adHocTool = {
18881
18896
  name: "ad_hoc",
18882
18897
  description: "Record work done outside the normal cycle. Creates a Done task with a lightweight build report, or associates work with an existing task if task_id is provided (without changing task status \u2014 use build_execute for status transitions). Use for quick fixes, bug patches, or ad-hoc changes. Does not call the Anthropic API.",
@@ -18908,6 +18923,16 @@ var adHocTool = {
18908
18923
  epic: {
18909
18924
  type: "string",
18910
18925
  description: 'Epic this relates to (default: "Platform").'
18926
+ },
18927
+ priority: {
18928
+ type: "string",
18929
+ enum: ["P0 Critical", "P1 High", "P2 Medium", "P3 Low"],
18930
+ description: "Task priority (default: P2 Medium). Use P1 for important fixes, P0 for critical incidents."
18931
+ },
18932
+ type: {
18933
+ type: "string",
18934
+ enum: ["task", "bug", "research", "discovery", "spike", "idea"],
18935
+ description: 'Task type (default: inferred from description \u2014 "fix"/"bug" \u2192 bug, "research"/"investigate" \u2192 research, otherwise task).'
18911
18936
  }
18912
18937
  },
18913
18938
  required: []
@@ -18923,6 +18948,14 @@ async function handleAdHoc(adapter2, config2, args) {
18923
18948
  if (!VALID_EFFORTS.includes(effortRaw)) {
18924
18949
  return errorResponse(`effort must be one of: ${VALID_EFFORTS.join(", ")}`);
18925
18950
  }
18951
+ const priorityRaw = args.priority || "P2 Medium";
18952
+ if (!VALID_PRIORITIES.includes(priorityRaw)) {
18953
+ return errorResponse(`priority must be one of: ${VALID_PRIORITIES.join(", ")}`);
18954
+ }
18955
+ const typeRaw = args.type || inferTaskType(title || (args.notes ?? ""));
18956
+ if (!VALID_TYPES.includes(typeRaw)) {
18957
+ return errorResponse(`type must be one of: ${VALID_TYPES.join(", ")}`);
18958
+ }
18926
18959
  const MAX_NOTES_LENGTH = 2e3;
18927
18960
  let rawNotes = args.notes?.trim();
18928
18961
  let notesTruncated = false;
@@ -18936,7 +18969,11 @@ async function handleAdHoc(adapter2, config2, args) {
18936
18969
  notes: rawNotes,
18937
18970
  effort: effortRaw,
18938
18971
  module: args.module,
18939
- epic: args.epic
18972
+ epic: args.epic,
18973
+ priority: priorityRaw,
18974
+ taskType: typeRaw,
18975
+ // PROJECT-SPECIFIC: owner resolved from config (PAPI_OWNER env var, default 'Cathal')
18976
+ owner: config2.projectOwner
18940
18977
  });
18941
18978
  if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
18942
18979
  try {
@@ -18949,8 +18986,11 @@ async function handleAdHoc(adapter2, config2, args) {
18949
18986
  }
18950
18987
  }
18951
18988
  const truncateWarning = notesTruncated ? ` (notes truncated to ${MAX_NOTES_LENGTH} chars)` : "";
18989
+ const taskModule = result.task.module || "Core";
18990
+ const typeLabel = result.task.taskType || typeRaw;
18952
18991
  return textResponse(
18953
- `**${result.task.id}:** "${result.task.title}" \u2014 recorded as ad-hoc (${effortRaw}). Build report attached.${truncateWarning}`
18992
+ `**${result.task.id}:** "${result.task.title}" recorded (${effortRaw}, ${priorityRaw}, ${typeLabel}, ${taskModule}).${truncateWarning} Build report attached.
18993
+ _To correct: board_edit ${result.task.id} with updated fields._`
18954
18994
  );
18955
18995
  }
18956
18996
 
@@ -19173,7 +19213,7 @@ async function applyReconcile(adapter2, corrections) {
19173
19213
  }
19174
19214
  return { applied, skipped, details, phaseChanges };
19175
19215
  }
19176
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0 Critical", "P1 High", "P2 Medium", "P3 Low"]);
19216
+ var VALID_PRIORITIES2 = /* @__PURE__ */ new Set(["P0 Critical", "P1 High", "P2 Medium", "P3 Low"]);
19177
19217
  var VALID_COMPLEXITIES = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
19178
19218
  async function prepareRetriage(adapter2) {
19179
19219
  const health = await adapter2.getCycleHealth();
@@ -19234,7 +19274,7 @@ async function applyRetriage(adapter2, retriages) {
19234
19274
  skipped++;
19235
19275
  continue;
19236
19276
  }
19237
- if (!VALID_PRIORITIES.has(r.priority)) {
19277
+ if (!VALID_PRIORITIES2.has(r.priority)) {
19238
19278
  details.push(`${r.taskId}: skipped \u2014 invalid priority "${r.priority}"`);
19239
19279
  skipped++;
19240
19280
  continue;
@@ -20205,11 +20245,41 @@ ${result.handoffRegenPrompt.userMessage}
20205
20245
  }
20206
20246
  }
20207
20247
  let autoReleaseNote = "";
20248
+ let batchSummaryNote = "";
20208
20249
  if (stage === "build-acceptance" && verdict === "accept" && result.newStatus === "Done" && result.currentCycle > 0) {
20209
20250
  try {
20210
20251
  const allTasks = await adapter2.queryBoard();
20211
20252
  const cycleTasks = allTasks.filter((t) => t.cycle === result.currentCycle);
20212
20253
  if (cycleTasks.length > 0 && cycleTasks.every((t) => t.status === "Done")) {
20254
+ try {
20255
+ const allReviews = await adapter2.getRecentReviews(200);
20256
+ const cycleReviews = allReviews.filter(
20257
+ (r) => r.cycle === result.currentCycle && r.stage === "build-acceptance"
20258
+ );
20259
+ const reviewsWithAutoReview = cycleReviews.filter((r) => r.autoReview);
20260
+ if (reviewsWithAutoReview.length > 0) {
20261
+ const verdictCounts = { pass: 0, warn: 0, fail: 0 };
20262
+ const findingsBySeverity = { error: 0, warning: 0, info: 0 };
20263
+ for (const r of reviewsWithAutoReview) {
20264
+ if (r.autoReview) {
20265
+ verdictCounts[r.autoReview.verdict] = (verdictCounts[r.autoReview.verdict] ?? 0) + 1;
20266
+ for (const f of r.autoReview.findings) {
20267
+ findingsBySeverity[f.severity] = (findingsBySeverity[f.severity] ?? 0) + 1;
20268
+ }
20269
+ }
20270
+ }
20271
+ const totalFindings = findingsBySeverity.error + findingsBySeverity.warning + findingsBySeverity.info;
20272
+ batchSummaryNote = `
20273
+
20274
+ ---
20275
+
20276
+ **Cycle ${result.currentCycle} Auto-Review Summary** (${reviewsWithAutoReview.length}/${cycleReviews.length} reviews had auto-review)
20277
+
20278
+ - Verdicts: ${verdictCounts.pass} pass, ${verdictCounts.warn} warn, ${verdictCounts.fail} fail
20279
+ ` + (totalFindings > 0 ? `- Findings: ${findingsBySeverity.error} error${findingsBySeverity.error !== 1 ? "s" : ""}, ${findingsBySeverity.warning} warning${findingsBySeverity.warning !== 1 ? "s" : ""}, ${findingsBySeverity.info} info` : "- No findings logged");
20280
+ }
20281
+ } catch {
20282
+ }
20213
20283
  const version = `v0.${result.currentCycle}.0`;
20214
20284
  const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
20215
20285
  const releaseResult = await createRelease(config2, baseBranch, version, adapter2);
@@ -20254,7 +20324,7 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
20254
20324
  - **Verdict:** ${result.verdict}
20255
20325
  - **Comments:** ${result.comments}
20256
20326
 
20257
- ${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
20327
+ ${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${batchSummaryNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
20258
20328
  );
20259
20329
  } catch (err) {
20260
20330
  return errorResponse(err instanceof Error ? err.message : String(err));
@@ -22453,5 +22523,123 @@ if (pkgVersion !== "unknown") {
22453
22523
  }
22454
22524
  })();
22455
22525
  }
22456
- var transport = new StdioServerTransport();
22457
- await server.connect(transport);
22526
+ var httpPortRaw = process.env["PAPI_HTTP_PORT"] ?? process.env["PORT"];
22527
+ var httpPort = httpPortRaw ? parseInt(httpPortRaw, 10) : void 0;
22528
+ var httpHost = process.env["PORT"] ? "0.0.0.0" : "127.0.0.1";
22529
+ if (httpPort) {
22530
+ if (isNaN(httpPort) || httpPort < 1 || httpPort > 65535) {
22531
+ process.stderr.write(`[papi] Invalid PAPI_HTTP_PORT: "${process.env.PAPI_HTTP_PORT}". Must be a number between 1 and 65535.
22532
+ `);
22533
+ process.exit(1);
22534
+ }
22535
+ const httpToken = process.env.PAPI_HTTP_TOKEN;
22536
+ if (!httpToken) {
22537
+ process.stderr.write("[papi] WARNING: PAPI_HTTP_TOKEN is not set. HTTP transport is unauthenticated \u2014 anyone with the URL can call your PAPI tools. Set PAPI_HTTP_TOKEN to a secret string.\n");
22538
+ }
22539
+ const createServerForRequest = () => {
22540
+ if (adapter && !setupError) {
22541
+ return createServer(adapter, config);
22542
+ }
22543
+ const errorServer = new Server2(
22544
+ { name: "papi", version: pkgVersion },
22545
+ { capabilities: { tools: {} } }
22546
+ );
22547
+ const errorMessage = setupError || "Unknown startup error";
22548
+ errorServer.setRequestHandler(ListToolsRequestSchema2, async () => ({
22549
+ tools: [{
22550
+ name: "setup",
22551
+ description: "PAPI is not connected \u2014 run this tool for setup instructions.",
22552
+ inputSchema: { type: "object", properties: {}, required: [] }
22553
+ }]
22554
+ }));
22555
+ errorServer.setRequestHandler(CallToolRequestSchema2, async () => ({
22556
+ content: [{
22557
+ type: "text",
22558
+ text: `# PAPI Connection Error
22559
+
22560
+ ${errorMessage}
22561
+
22562
+ ## Quick Fix
22563
+
22564
+ If you haven't set up PAPI yet:
22565
+ 1. Go to https://getpapi.ai/login and sign up
22566
+ 2. Complete the onboarding wizard \u2014 it generates your config
22567
+ 3. Copy the config to your project and restart your AI tool
22568
+
22569
+ If you already have an account, check that both **PAPI_PROJECT_ID** and **PAPI_DATA_API_KEY** are set in your .mcp.json env config.`
22570
+ }]
22571
+ }));
22572
+ return errorServer;
22573
+ };
22574
+ const httpServer = createHttpServer((req, res) => {
22575
+ if (req.method === "GET" && req.url === "/healthz") {
22576
+ res.writeHead(200, { "Content-Type": "text/plain" });
22577
+ res.end("ok");
22578
+ return;
22579
+ }
22580
+ if (httpToken) {
22581
+ const authHeader = req.headers["authorization"] ?? "";
22582
+ const provided = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
22583
+ if (provided !== httpToken) {
22584
+ res.writeHead(401, { "Content-Type": "application/json" });
22585
+ res.end(JSON.stringify({ error: "Unauthorized" }));
22586
+ return;
22587
+ }
22588
+ }
22589
+ if (req.url === "/mcp" || req.url === "/sse") {
22590
+ if (req.method === "POST") {
22591
+ const chunks = [];
22592
+ req.on("data", (chunk) => chunks.push(chunk));
22593
+ req.on("end", () => {
22594
+ let parsedBody;
22595
+ try {
22596
+ parsedBody = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
22597
+ } catch {
22598
+ res.writeHead(400, { "Content-Type": "application/json" });
22599
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
22600
+ return;
22601
+ }
22602
+ (async () => {
22603
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
22604
+ const reqServer = createServerForRequest();
22605
+ await reqServer.connect(transport);
22606
+ await transport.handleRequest(req, res, parsedBody);
22607
+ await reqServer.close();
22608
+ })().catch((err) => {
22609
+ process.stderr.write(`[papi] HTTP transport error: ${err instanceof Error ? err.message : String(err)}
22610
+ `);
22611
+ if (!res.headersSent) {
22612
+ res.writeHead(500, { "Content-Type": "application/json" });
22613
+ res.end(JSON.stringify({ error: "Internal server error" }));
22614
+ }
22615
+ });
22616
+ });
22617
+ return;
22618
+ }
22619
+ (async () => {
22620
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
22621
+ const reqServer = createServerForRequest();
22622
+ await reqServer.connect(transport);
22623
+ await transport.handleRequest(req, res);
22624
+ await reqServer.close();
22625
+ })().catch((err) => {
22626
+ process.stderr.write(`[papi] HTTP transport error: ${err instanceof Error ? err.message : String(err)}
22627
+ `);
22628
+ if (!res.headersSent) {
22629
+ res.writeHead(500, { "Content-Type": "application/json" });
22630
+ res.end(JSON.stringify({ error: "Internal server error" }));
22631
+ }
22632
+ });
22633
+ return;
22634
+ }
22635
+ res.writeHead(404, { "Content-Type": "text/plain" });
22636
+ res.end("Not found");
22637
+ });
22638
+ httpServer.listen(httpPort, httpHost, () => {
22639
+ process.stderr.write(`[papi] HTTP transport listening on http://${httpHost}:${httpPort}/mcp
22640
+ `);
22641
+ });
22642
+ } else {
22643
+ const transport = new StdioServerTransport();
22644
+ await server.connect(transport);
22645
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",