@papi-ai/server 0.7.77 → 0.7.79

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.
@@ -560,11 +560,13 @@ function ensureTagAtHead(cwd, tag, message) {
560
560
  message: `tag "${tag}" already exists but points at ${target ? target.slice(0, 7) : "an unknown commit"}, not the current HEAD (${head ? head.slice(0, 7) : "unknown"}). If it is left over from an aborted release, delete it and re-run release: \`git tag -d ${tag}\` (and \`git push origin :refs/tags/${tag}\` if it was pushed). Otherwise use a different version.`
561
561
  };
562
562
  }
563
- function getLatestTag(cwd) {
563
+ function getLatestTag(cwd, timeoutMs) {
564
564
  try {
565
565
  return execFileSync("git", ["describe", "--tags", "--abbrev=0"], {
566
566
  cwd,
567
- encoding: "utf-8"
567
+ encoding: "utf-8",
568
+ stdio: ["ignore", "pipe", "pipe"],
569
+ ...timeoutMs != null ? { timeout: timeoutMs } : {}
568
570
  }).trim() || null;
569
571
  } catch {
570
572
  return null;
@@ -806,20 +808,26 @@ function findTaskCommitsOnBase(cwd, preferredBase, displayIds) {
806
808
  try {
807
809
  raw = execFileSync(
808
810
  "git",
809
- ["log", base, "--format=%h%x01%s", "-n", "1000"],
811
+ ["log", base, "--format=%h%x01%s%x01%b%x02", "-n", "1000"],
810
812
  { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
811
813
  );
812
814
  } catch {
813
815
  return out;
814
816
  }
815
- const commits = raw.split("\n").map((line) => {
816
- const idx = line.indexOf("");
817
- if (idx === -1) return null;
818
- return { hash: line.slice(0, idx).trim(), subject: line.slice(idx + 1).trim() };
819
- }).filter((c) => c !== null && c.hash !== "");
817
+ const commits = raw.split("").map((record) => {
818
+ const parts = record.split("");
819
+ if (parts.length < 2) return null;
820
+ return {
821
+ hash: parts[0].trim(),
822
+ subject: parts[1].trim(),
823
+ body: (parts[2] ?? "").trim()
824
+ };
825
+ }).filter(
826
+ (c) => c !== null && c.hash !== ""
827
+ );
820
828
  for (const displayId of displayIds) {
821
829
  const re = new RegExp(`(^|[^\\w-])${escapeRegexLiteral(displayId)}([^\\w-]|$)`);
822
- const hit = commits.find((c) => re.test(c.subject));
830
+ const hit = commits.find((c) => re.test(c.subject)) ?? commits.find((c) => re.test(c.body));
823
831
  if (!hit) continue;
824
832
  const prMatch = hit.subject.match(/#(\d+)/);
825
833
  out.set(displayId, {
@@ -1289,10 +1297,39 @@ var init_proxy_adapter = __esm({
1289
1297
  endpoint;
1290
1298
  apiKey;
1291
1299
  projectId;
1300
+ onAuthRejected;
1292
1301
  constructor(config) {
1293
1302
  this.endpoint = config.endpoint.replace(/\/$/, "");
1294
1303
  this.apiKey = config.apiKey;
1295
1304
  this.projectId = config.projectId ?? "";
1305
+ this.onAuthRejected = config.onAuthRejected;
1306
+ }
1307
+ /**
1308
+ * task-1773: bearer-only auth probe. Hits the USER-scoped `project-list` route
1309
+ * (no projectId needed), so it answers exactly one question: does the proxy
1310
+ * still accept this bearer?
1311
+ *
1312
+ * Returns the HTTP status, or 0 when the call could not be made at all
1313
+ * (network error / timeout). Callers MUST treat 0 — and any status that is
1314
+ * neither 2xx nor 401 — as "no signal", never as a rejection: a proxy outage
1315
+ * must not masquerade as a revoked token and force every user to re-auth.
1316
+ */
1317
+ async probeBearerStatus() {
1318
+ try {
1319
+ const response = await fetch(`${this.endpoint}/project-list`, {
1320
+ method: "POST",
1321
+ headers: {
1322
+ "Content-Type": "application/json",
1323
+ "Authorization": `Bearer ${this.apiKey}`
1324
+ },
1325
+ body: "{}",
1326
+ signal: AbortSignal.timeout(5e3)
1327
+ });
1328
+ if (response.status === 401) this.onAuthRejected?.();
1329
+ return response.status;
1330
+ } catch {
1331
+ return 0;
1332
+ }
1296
1333
  }
1297
1334
  /** Resolved project ID — available after ensureProject() completes. */
1298
1335
  getProjectId() {
@@ -1309,7 +1346,8 @@ var init_proxy_adapter = __esm({
1309
1346
  return wrapWithForwarding(new _ProxyPapiAdapter({
1310
1347
  endpoint: this.endpoint,
1311
1348
  apiKey: this.apiKey,
1312
- projectId
1349
+ projectId,
1350
+ onAuthRejected: this.onAuthRejected
1313
1351
  }));
1314
1352
  }
1315
1353
  /**
@@ -1396,6 +1434,7 @@ var init_proxy_adapter = __esm({
1396
1434
  message = errorBody;
1397
1435
  }
1398
1436
  if (response.status === 401) {
1437
+ this.onAuthRejected?.();
1399
1438
  throw new Error(
1400
1439
  `Auth: Invalid API key \u2014 PAPI_DATA_API_KEY was rejected by the proxy.
1401
1440
  This usually means the key was revoked or replaced. Mint a fresh key in the Connect panel on your PAPI dashboard (https://getpapi.ai/hub), then update PAPI_DATA_API_KEY in your .mcp.json.
@@ -1974,6 +2013,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
1974
2013
  } catch {
1975
2014
  message = errorBody;
1976
2015
  }
2016
+ if (response.status === 401) this.onAuthRejected?.();
1977
2017
  throw new Error(`Proxy error (${response.status}) on ${route}: ${message}`);
1978
2018
  }
1979
2019
  const body = await response.json();
@@ -2290,9 +2330,12 @@ function parsePlanningLog(content, activeDecisionsContent, cycleLogContent) {
2290
2330
  var VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
2291
2331
  var SECTION_HEADERS = [
2292
2332
  "SCOPE (DO THIS)",
2333
+ "WHY NOT SIMPLER",
2293
2334
  "SCOPE BOUNDARY (DO NOT DO THIS)",
2294
2335
  "ACCEPTANCE CRITERIA",
2336
+ "PRE-MORTEM",
2295
2337
  "SECURITY CONSIDERATIONS",
2338
+ "DEPLOY VERIFICATION",
2296
2339
  "PRE-BUILD VERIFICATION",
2297
2340
  "FILES LIKELY TOUCHED",
2298
2341
  "EFFORT"
@@ -2331,7 +2374,7 @@ function parseBulletsOnly(text) {
2331
2374
  return text.split("\n").filter((l) => /^\s*-\s/.test(l)).map((l) => l.replace(/^\s*-\s*/, "").trim()).filter((l) => l.length > 0);
2332
2375
  }
2333
2376
  function parseChecklist(text) {
2334
- return text.split("\n").map((l) => l.replace(/^\s*\[[ x]]\s*/, "").trim()).filter((l) => l.length > 0);
2377
+ return text.split("\n").map((l) => l.replace(/^\s*(?:[-*+]\s*)?(?:\[[ xX]\]\s*)?/, "").trim()).filter((l) => l.length > 0);
2335
2378
  }
2336
2379
  function parseBuildHandoff(markdown) {
2337
2380
  if (typeof markdown !== "string" || !markdown.trim()) return null;
@@ -2959,13 +3002,21 @@ var EFFORT_SCALE = {
2959
3002
  XL: 5
2960
3003
  };
2961
3004
  function effortOrdinal(effort) {
3005
+ if (typeof effort !== "string") return void 0;
2962
3006
  const normalized = effort.trim().toUpperCase();
2963
3007
  return EFFORT_SCALE[normalized];
2964
3008
  }
3009
+ function isUnparsedEffort(effort) {
3010
+ if (typeof effort !== "string" || effort.trim().length === 0) return false;
3011
+ return effortOrdinal(effort) === void 0;
3012
+ }
2965
3013
  function calculateCycleMetrics(reports, currentCycle, window = 5) {
2966
3014
  const recentReports = reports.filter(
2967
3015
  (r) => r.cycle > currentCycle - window && r.cycle <= currentCycle
2968
3016
  );
3017
+ const unparsedEffortCount = recentReports.filter(
3018
+ (r) => isUnparsedEffort(r.actualEffort) || isUnparsedEffort(r.estimatedEffort)
3019
+ ).length;
2969
3020
  const perCycle = /* @__PURE__ */ new Map();
2970
3021
  for (const r of recentReports) {
2971
3022
  const group = perCycle.get(r.cycle) ?? [];
@@ -3002,7 +3053,7 @@ function calculateCycleMetrics(reports, currentCycle, window = 5) {
3002
3053
  effortPoints: reps.reduce((s, r) => s + (effortOrdinal(r.actualEffort) ?? 0), 0)
3003
3054
  });
3004
3055
  }
3005
- return { accuracy, velocity };
3056
+ return { accuracy, velocity, unparsedEffortCount };
3006
3057
  }
3007
3058
  function serializeAccuracyRow(a) {
3008
3059
  return `| ${a.cycle} | ${a.reports} | ${a.matchRate}% | ${a.mae} | ${a.bias >= 0 ? "+" : ""}${a.bias} |`;
@@ -4428,6 +4479,7 @@ async function createAdapter(optionsOrType, maybePapiDir) {
4428
4479
  case "pg": {
4429
4480
  const { PgAdapter, PgPapiAdapter, configFromEnv } = await import("@papi-ai/adapter-pg");
4430
4481
  let projectId = process.env["PAPI_PROJECT_ID"];
4482
+ const projectIdWasPreSupplied = Boolean(projectId);
4431
4483
  const projectRoot = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? process.cwd();
4432
4484
  let rootHash = null;
4433
4485
  let originUrl = null;
@@ -4491,6 +4543,26 @@ async function createAdapter(optionsOrType, maybePapiDir) {
4491
4543
  }
4492
4544
  const config = papiEndpoint ? { connectionString: papiEndpoint } : configFromEnv();
4493
4545
  validateDatabaseUrl(config.connectionString);
4546
+ if (projectIdWasPreSupplied) {
4547
+ const ownershipProbe = new PgAdapter(config);
4548
+ try {
4549
+ const owned = await ownershipProbe.findProjectById(projectId, resolveUserId);
4550
+ if (!owned) {
4551
+ throw new Error(
4552
+ `PAPI_PROJECT_ID ${projectId} does not belong to you.
4553
+
4554
+ The project exists under a different owner, or does not exist at all. PAPI refuses to attach to a project you do not own \u2014 writing to it would put your cycles, tasks and Active Decisions into somebody else's project.
4555
+
4556
+ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the project from your git remote), then reconnect.`
4557
+ );
4558
+ }
4559
+ } finally {
4560
+ try {
4561
+ await ownershipProbe.close();
4562
+ } catch {
4563
+ }
4564
+ }
4565
+ }
4494
4566
  const { ensureSchema } = await import("@papi-ai/adapter-pg");
4495
4567
  try {
4496
4568
  await ensureSchema(config);