@papi-ai/server 0.7.106 → 0.7.110

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/README.md CHANGED
@@ -43,6 +43,14 @@ PAPI collects anonymous usage data (tool name, duration, project UUID — no cod
43
43
 
44
44
  PAPI is configured with `PAPI_PROJECT_ID` and `PAPI_DATA_API_KEY` — both are generated by the onboarding wizard at [getpapi.ai](https://getpapi.ai/) and pasted into your `.mcp.json`. If those env vars aren't set, PAPI will fall back to local file storage (md mode) and emit a stderr warning that your cycles aren't visible on the dashboard. To get on the dashboard, sign up at [getpapi.ai](https://getpapi.ai/) and use the config it gives you.
45
45
 
46
+ ### Stdio idle shutdown
47
+
48
+ The stdio server exits cleanly after two minutes without input, including its
49
+ database adapter pool, so abandoned client sessions do not accumulate. Set
50
+ `PAPI_STDIO_IDLE_TIMEOUT_MS=0` to disable this behavior, or provide another
51
+ non-negative millisecond value in the `.mcp.json` environment block. This
52
+ setting applies only to stdio mode; hosted HTTP instances are not idle-killed.
53
+
46
54
  ## License
47
55
 
48
56
  [Elastic License 2.0](https://www.elastic.co/licensing/elastic-license) — free to use, self-host, and modify. Commercial hosting requires a license.
@@ -12,6 +12,10 @@ var __export = (target, all) => {
12
12
  function isSelfHostedDeployment(value) {
13
13
  return value === "1" || value?.toLowerCase() === "true";
14
14
  }
15
+ function isFossilSupabaseReference(value) {
16
+ if (!value) return false;
17
+ return value.includes(".pooler.supabase.com") || value.includes(DECOMMISSIONED_SUPABASE_PROJECT_REF);
18
+ }
15
19
  function optionalBoolean(value, fallback) {
16
20
  if (value == null || value.trim() === "") return fallback;
17
21
  return isSelfHostedDeployment(value);
@@ -94,10 +98,11 @@ function calculateCycleMetrics(reports, currentCycle, window = 5) {
94
98
  }
95
99
  return { accuracy, velocity, unparsedEffortCount };
96
100
  }
97
- var CAPABILITY_REGISTRY, CAPABILITY_KEYS, HOSTED_APP_URL, HOSTED_MCP_URL, HOSTED_SUPABASE_URL, WAB_WINDOW_DAYS, WAB_WEEK_MS, EFFORT_SCALE;
101
+ var DECOMMISSIONED_SUPABASE_PROJECT_REF, CAPABILITY_REGISTRY, CAPABILITY_KEYS, HOSTED_APP_URL, HOSTED_MCP_URL, HOSTED_SUPABASE_URL, WAB_WINDOW_DAYS, WAB_WEEK_MS, EFFORT_SCALE;
98
102
  var init_dist = __esm({
99
103
  "../shared/dist/index.js"() {
100
104
  "use strict";
105
+ DECOMMISSIONED_SUPABASE_PROJECT_REF = "guewgygcpcmrcoppihzx";
101
106
  CAPABILITY_REGISTRY = [
102
107
  // task-3384: the PLAN step. The landing page has advertised these three since
103
108
  // the v5 control beat (components/marketing/landing/ControlC.tsx) with a code
@@ -2854,6 +2859,11 @@ var PLACEHOLDER_PATTERNS = [
2854
2859
  ];
2855
2860
  function validateDatabaseUrl(connectionString) {
2856
2861
  const lower = connectionString.toLowerCase().trim();
2862
+ if (isFossilSupabaseReference(connectionString)) {
2863
+ throw new Error(
2864
+ "This config points at PAPI's old hosted database, which was migrated.\nRegenerate your config at https://getpapi.ai \u2014 your projects and data are intact."
2865
+ );
2866
+ }
2857
2867
  if (PLACEHOLDER_PATTERNS.some((p) => lower.includes(p.toLowerCase()))) {
2858
2868
  throw new Error(
2859
2869
  "DATABASE_URL contains a placeholder value and is not configured.\nReplace it with your actual Supabase connection string in .mcp.json.\nIf you don't have one yet, contact the PAPI admin for access."
@@ -2868,10 +2878,12 @@ Check your .mcp.json configuration.`
2868
2878
  }
2869
2879
  }
2870
2880
  var _connectionStatus = "offline";
2881
+ var _lastAdapterType = null;
2871
2882
  var _pathIdentityChecked = false;
2872
2883
  async function createAdapter(optionsOrType, maybePapiDir) {
2873
2884
  const options = typeof optionsOrType === "string" ? { adapterType: optionsOrType, papiDir: maybePapiDir } : optionsOrType;
2874
2885
  const { adapterType, papiDir, papiEndpoint } = options;
2886
+ _lastAdapterType = adapterType;
2875
2887
  switch (adapterType) {
2876
2888
  case "pg": {
2877
2889
  const { PgAdapter, PgPapiAdapter, configFromEnv } = await import("@papi-ai/adapter-pg");
package/dist/index.js CHANGED
@@ -14,6 +14,10 @@ import { randomUUID } from "crypto";
14
14
  function isSelfHostedDeployment(value) {
15
15
  return value === "1" || value?.toLowerCase() === "true";
16
16
  }
17
+ function isFossilSupabaseReference(value) {
18
+ if (!value) return false;
19
+ return value.includes(".pooler.supabase.com") || value.includes(DECOMMISSIONED_SUPABASE_PROJECT_REF);
20
+ }
17
21
  function isCapabilityEnabled(caps, key) {
18
22
  const stored = caps?.[key];
19
23
  if (stored != null) return stored !== false;
@@ -788,10 +792,11 @@ function parsePhaseBlock(block) {
788
792
  if (isNaN(order)) return null;
789
793
  return { id, slug, label, description, status, order };
790
794
  }
791
- var CAPABILITY_REGISTRY, CAPABILITY_KEYS, ASSIGNABLE_CONTRIBUTOR_ROLES, SENSITIVE_CHANGELOG_PATTERNS, CONTRIBUTOR_PRICING_URL, PROPOSAL_EVENT_TYPES, HOSTED_APP_URL, HOSTED_MCP_URL, HOSTED_SUPABASE_URL, SEVERITY_ORDER, NONE_RE, AD_HOC_PLACEHOLDER_RE, NONE_LEAD_RE, CONTRAST_LEAD_RE, MIN_REASON_LENGTH, LOW_SEVERITIES, WAB_WINDOW_DAYS, WAB_DEFAULT_WEEKS, WAB_WEEK_MS, PRODUCT_MARKER, MECHANICS_MARKER, CHECK_VALUE_MAX, VALID_EFFORT_SIZES, SECTION_HEADERS, VALID_EFFORT_SIZES2, EFFORT_SCALE, NONE_PATTERN, NONE_PATTERN2, VALID_STATUSES, PHASES_START, PHASES_END;
795
+ var DECOMMISSIONED_SUPABASE_PROJECT_REF, CAPABILITY_REGISTRY, CAPABILITY_KEYS, ASSIGNABLE_CONTRIBUTOR_ROLES, SENSITIVE_CHANGELOG_PATTERNS, CONTRIBUTOR_PRICING_URL, PROPOSAL_EVENT_TYPES, HOSTED_APP_URL, HOSTED_MCP_URL, HOSTED_SUPABASE_URL, SEVERITY_ORDER, NONE_RE, AD_HOC_PLACEHOLDER_RE, NONE_LEAD_RE, CONTRAST_LEAD_RE, MIN_REASON_LENGTH, LOW_SEVERITIES, WAB_WINDOW_DAYS, WAB_DEFAULT_WEEKS, WAB_WEEK_MS, PRODUCT_MARKER, MECHANICS_MARKER, CHECK_VALUE_MAX, VALID_EFFORT_SIZES, SECTION_HEADERS, VALID_EFFORT_SIZES2, EFFORT_SCALE, NONE_PATTERN, NONE_PATTERN2, VALID_STATUSES, PHASES_START, PHASES_END;
792
796
  var init_dist = __esm({
793
797
  "../shared/dist/index.js"() {
794
798
  "use strict";
799
+ DECOMMISSIONED_SUPABASE_PROJECT_REF = "guewgygcpcmrcoppihzx";
795
800
  CAPABILITY_REGISTRY = [
796
801
  // task-3384: the PLAN step. The landing page has advertised these three since
797
802
  // the v5 control beat (components/marketing/landing/ControlC.tsx) with a code
@@ -7175,6 +7180,11 @@ var PLACEHOLDER_PATTERNS = [
7175
7180
  ];
7176
7181
  function validateDatabaseUrl(connectionString) {
7177
7182
  const lower = connectionString.toLowerCase().trim();
7183
+ if (isFossilSupabaseReference(connectionString)) {
7184
+ throw new Error(
7185
+ "This config points at PAPI's old hosted database, which was migrated.\nRegenerate your config at https://getpapi.ai \u2014 your projects and data are intact."
7186
+ );
7187
+ }
7178
7188
  if (PLACEHOLDER_PATTERNS.some((p) => lower.includes(p.toLowerCase()))) {
7179
7189
  throw new Error(
7180
7190
  "DATABASE_URL contains a placeholder value and is not configured.\nReplace it with your actual Supabase connection string in .mcp.json.\nIf you don't have one yet, contact the PAPI admin for access."
@@ -7189,6 +7199,10 @@ Check your .mcp.json configuration.`
7189
7199
  }
7190
7200
  }
7191
7201
  var _connectionStatus = "offline";
7202
+ var _lastAdapterType = null;
7203
+ function getLastAdapterType() {
7204
+ return _lastAdapterType;
7205
+ }
7192
7206
  var _pathIdentityChecked = false;
7193
7207
  function getConnectionStatus() {
7194
7208
  return _connectionStatus;
@@ -7196,6 +7210,7 @@ function getConnectionStatus() {
7196
7210
  async function createAdapter(optionsOrType, maybePapiDir) {
7197
7211
  const options = typeof optionsOrType === "string" ? { adapterType: optionsOrType, papiDir: maybePapiDir } : optionsOrType;
7198
7212
  const { adapterType, papiDir, papiEndpoint } = options;
7213
+ _lastAdapterType = adapterType;
7199
7214
  switch (adapterType) {
7200
7215
  case "pg": {
7201
7216
  const { PgAdapter, PgPapiAdapter, configFromEnv } = await import("@papi-ai/adapter-pg");
@@ -7543,9 +7558,10 @@ function resolveServerName(serverName) {
7543
7558
  const candidate = serverName?.trim();
7544
7559
  return candidate && SAFE_SERVER_NAME.test(candidate) ? candidate : DEFAULT_SERVER_NAME;
7545
7560
  }
7546
- function formatConnectionBanner(identity) {
7561
+ function formatConnectionBanner(identity, health = "connected") {
7547
7562
  const projectIdShort = identity.projectId ? ` (${identity.projectId.slice(0, 8)})` : "";
7548
- return `[papi] Connected \u2014 server: ${resolveServerName(identity.serverName)}, project: ${identity.projectName}${projectIdShort}, adapter: ${identity.adapterType}, v${identity.pkgVersion}`;
7563
+ const lead = health === "degraded" ? "Degraded \u2014 data may be stale" : health === "offline" ? "Offline \u2014 no database connection" : "Connected";
7564
+ return `[papi] ${lead} \u2014 server: ${resolveServerName(identity.serverName)}, project: ${identity.projectName}${projectIdShort}, adapter: ${identity.adapterType}, v${identity.pkgVersion}`;
7549
7565
  }
7550
7566
 
7551
7567
  // src/server.ts
@@ -11019,7 +11035,7 @@ function isBlockerResolved(blocker, ctx) {
11019
11035
  if (decision?.outcome && resolvedOutcomes.has(decision.outcome)) return true;
11020
11036
  if (decision?.resolutionState === "resolved") return true;
11021
11037
  const hasRecentEvent = ctx.decisionEvents.some(
11022
- (e) => idMatches(e.decisionId, blocker.ref) && e.cycle >= blocker.blockedCycle && !PROPOSAL_EVENT_TYPES.includes(e.eventType)
11038
+ (e) => idMatches(e.decisionId, blocker.ref) && e.cycle >= blocker.blockedCycle && !PROPOSAL_EVENT_TYPES.includes(e.eventType) && e.source !== "build_complete"
11023
11039
  );
11024
11040
  return hasRecentEvent;
11025
11041
  }
@@ -12410,6 +12426,19 @@ ${cleanContent}`;
12410
12426
  `task-3670 self-heal: ${repairedCycles.length} task(s) had lost cycle membership and were re-assigned (${repairedCycles.join(", ")})`
12411
12427
  );
12412
12428
  }
12429
+ let health;
12430
+ try {
12431
+ health = await adapter2.getCycleHealth?.();
12432
+ } catch (err) {
12433
+ verifyWarnings.push(
12434
+ `task-3733: could not re-verify the cycle row after write (${err instanceof Error ? err.message : String(err)}) \u2014 cycle membership was written and repaired, but this final check could not confirm it.`
12435
+ );
12436
+ }
12437
+ if (health && health.totalCycles !== newCycleNumber) {
12438
+ throw new Error(
12439
+ `Plan apply committed, but the cycle row for Cycle ${newCycleNumber} did not take: getCycleHealth() reports the caller's current cycle as ${health.totalCycles}. Task assignments were written and repaired, but the cycle entity itself is stale \u2014 investigate the planWriteBack cycle upsert before trusting this apply.`
12440
+ );
12441
+ }
12413
12442
  const allWarnings = [...result.warnings, ...verifyWarnings, ...earlyCreateWarnings];
12414
12443
  const handoffCount = data.cycleHandoffs?.length ?? 0;
12415
12444
  const correctionCount = data.boardCorrections?.length ?? 0;
@@ -22097,60 +22126,87 @@ Releasing to a non-base branch (e.g. dev) is unaffected.` + resolutionNote
22097
22126
  }
22098
22127
  const releaseRole = callerRole === "release_manager" ? "release manager" : "editor";
22099
22128
  tracker.mark("contributor-pr-reconciliation");
22129
+ const closeContributorCycle = (cycleOverride) => closeCycleState(
22130
+ config2,
22131
+ adapter2,
22132
+ version,
22133
+ cycleOverride ?? cycleToClose ?? void 0,
22134
+ {
22135
+ force: force ?? false,
22136
+ skipVersion: skipVersion ?? false,
22137
+ callerUserId: gate.callerUserId
22138
+ }
22139
+ );
22140
+ let reconciled = null;
22100
22141
  try {
22101
- const reconciled = await reconcileContributorReleasePrs(config2, adapter2, gate.callerUserId, cycleToClose);
22102
- if (reconciled.merged.length > 0) {
22103
- const latest = reconciled.merged[0];
22104
- let caps2 = {};
22105
- try {
22106
- const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
22107
- caps2 = info?.capabilities ?? {};
22108
- } catch {
22109
- caps2 = {};
22110
- }
22111
- await recordReadinessVerified(tracker);
22112
- await recordQualityGate(tracker, evaluateReleaseGate(caps2, config2.gateCommand, gateResult), caps2);
22113
- await completeRelease(tracker, {
22114
- cycleClosed: latest.cycle,
22115
- version,
22116
- caps: caps2,
22117
- branchMerges: latest.branch ? [{ branch: latest.branch, prUrl: latest.prUrl }] : [],
22118
- changelogEmitted: false
22119
- });
22120
- const localHandoff = isHostedTransport() ? `
22142
+ reconciled = await reconcileContributorReleasePrs(config2, adapter2, gate.callerUserId, cycleToClose);
22143
+ } catch (err) {
22144
+ console.error(`[release] contributor PR reconciliation failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
22145
+ }
22146
+ if (reconciled?.merged.length) {
22147
+ const latest = reconciled.merged[0];
22148
+ let caps2 = {};
22149
+ try {
22150
+ const info = adapter2.getProjectInfo ? await adapter2.getProjectInfo() : null;
22151
+ caps2 = info?.capabilities ?? {};
22152
+ } catch {
22153
+ caps2 = {};
22154
+ }
22155
+ let closed;
22156
+ try {
22157
+ closed = await closeContributorCycle(latest.cycle);
22158
+ } catch (err) {
22159
+ return errorResponse(err instanceof Error ? err.message : String(err));
22160
+ }
22161
+ await recordReadinessVerified(tracker);
22162
+ await recordQualityGate(tracker, evaluateReleaseGate(caps2, config2.gateCommand, gateResult), caps2);
22163
+ await completeRelease(tracker, {
22164
+ cycleClosed: closed.resolvedCycleNum > 0 ? closed.resolvedCycleNum : latest.cycle,
22165
+ version,
22166
+ caps: caps2,
22167
+ branchMerges: latest.branch ? [{ branch: latest.branch, prUrl: latest.prUrl }] : [],
22168
+ changelogEmitted: false
22169
+ });
22170
+ const localHandoff = isHostedTransport() ? `
22121
22171
  ${buildHostedGitDirective({
22122
- version,
22123
- branch,
22124
- skipVersion: skipVersion ?? false,
22125
- harnessCanBuild: detectHarness(clientName).build,
22126
- harnessKnown: detectHarness(clientName).known,
22127
- harnessLabel: detectHarness(clientName).label,
22128
- changelogSection: null
22129
- })}` : "";
22130
- return textResponse(
22131
- `## Release ${version} \u2014 contributor PR merged
22172
+ version,
22173
+ branch,
22174
+ skipVersion: skipVersion ?? false,
22175
+ harnessCanBuild: detectHarness(clientName).build,
22176
+ harnessKnown: detectHarness(clientName).known,
22177
+ harnessLabel: detectHarness(clientName).label,
22178
+ changelogSection: null
22179
+ })}` : "";
22180
+ const cycleLabel = closed.resolvedCycleNum > 0 ? `Cycle ${closed.resolvedCycleNum}` : latest.cycle ? `Cycle ${latest.cycle}` : "The contributor cycle";
22181
+ return textResponse(
22182
+ `## Release ${version} \u2014 contributor PR merged
22132
22183
 
22133
22184
  PAPI detected that ${latest.prUrl} was merged on GitHub and reconciled the contributor release. GitHub granted the merge permission; your PAPI **${releaseRole}** role granted the release workflow.
22134
22185
 
22135
- ${latest.cycle ? `Cycle ${latest.cycle}` : "The contributor cycle"} remains **complete**, and the release is now recorded as **released** in PAPI.` + localHandoff + `
22186
+ ${cycleLabel} remains **complete**, and the release is now recorded as **released** in PAPI.` + localHandoff + `
22136
22187
 
22137
22188
  Next: run \`plan\` to start your next cycle.`
22138
- );
22189
+ );
22190
+ }
22191
+ if (reconciled?.open.length) {
22192
+ const openCycle = cycleToClose !== null ? reconciled.open.find((pr) => pr.cycle === cycleToClose)?.cycle ?? null : reconciled.open[0].cycle;
22193
+ if (openCycle !== null && openCycle !== void 0) {
22194
+ try {
22195
+ await closeContributorCycle(openCycle);
22196
+ } catch (err) {
22197
+ return errorResponse(err instanceof Error ? err.message : String(err));
22198
+ }
22139
22199
  }
22140
- if (reconciled.open.length > 0) {
22141
- const prLines = reconciled.open.map((p) => `- ${p.prUrl}`).join("\n");
22142
- return textResponse(
22143
- `## Release ${version} \u2014 contributor PR awaiting merge
22200
+ const prLines = reconciled.open.map((p) => `- ${p.prUrl}`).join("\n");
22201
+ return textResponse(
22202
+ `## Release ${version} \u2014 contributor PR awaiting merge
22144
22203
 
22145
22204
  ${prLines}
22146
22205
 
22147
22206
  Your PAPI **${releaseRole}** role allowed this release PR. Merge permission is controlled separately by GitHub: if GitHub lets you merge, review and merge it there; otherwise a repository maintainer must merge it.
22148
22207
 
22149
22208
  After it is merged, run \`release\` again so PAPI can detect the merge and finish the release record.`
22150
- );
22151
- }
22152
- } catch (err) {
22153
- console.error(`[release] contributor PR reconciliation failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
22209
+ );
22154
22210
  }
22155
22211
  if (isHostedTransport()) {
22156
22212
  return errorResponse(
@@ -23089,7 +23145,7 @@ function getUnresolvedDeps(task, allTasks) {
23089
23145
  const taskStatusMap = new Map(allTasks.map((t) => [t.id, t.status]));
23090
23146
  return deps.filter((depId) => {
23091
23147
  const status = taskStatusMap.get(depId);
23092
- return status !== "Done";
23148
+ return status !== "Done" && status !== "Cancelled" && status !== "Archived";
23093
23149
  });
23094
23150
  }
23095
23151
  function resolveDepCycleBranch(cycleBranches, cycleNumber, upstreamModule, dependentModule) {
@@ -29912,6 +29968,30 @@ async function handleReviewClaim(adapter2, config2, args) {
29912
29968
  import { randomUUID as randomUUID12 } from "crypto";
29913
29969
  import { access as access2, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
29914
29970
  import path6 from "path";
29971
+ init_dist();
29972
+ var FOSSIL_CONFIG_MESSAGE = [
29973
+ "This config points at PAPI's old hosted database, which was migrated.",
29974
+ "Regenerate your config at https://getpapi.ai \u2014 your projects and data are intact."
29975
+ ].join("\n");
29976
+ function extractPapiOwnedConfigText(existingConfig, agent) {
29977
+ if (agent === "codex") {
29978
+ const match = existingConfig.match(/\[mcp_servers\.papi\][\s\S]*?(?=\n\[|$)/);
29979
+ return match ? match[0] : existingConfig;
29980
+ }
29981
+ if (agent === "hermes") {
29982
+ const match = existingConfig.match(/^[ \t]*papi[ \t]*:[\s\S]*?(?=\n[ \t]{0,2}\S[^\n]*:[ \t]*$|$)/m);
29983
+ return match ? match[0] : existingConfig;
29984
+ }
29985
+ try {
29986
+ const parsed = JSON.parse(existingConfig);
29987
+ const container = parsed.mcpServers ?? parsed.servers ?? parsed.mcp;
29988
+ const papiEntry = container?.papi;
29989
+ const env = papiEntry?.env ?? papiEntry?.environment;
29990
+ if (env) return JSON.stringify(env);
29991
+ } catch {
29992
+ }
29993
+ return existingConfig;
29994
+ }
29915
29995
  var initTool = {
29916
29996
  name: "init",
29917
29997
  description: "Write the MCP config file that connects this project to PAPI. Generates .mcp.json, or the equivalent for whichever MCP client you use \u2014 Claude Code, Cursor, VS Code, Windsurf, OpenCode, Amazon Q, Kilo Code, Gemini CLI, Codex CLI, or Hermes Agent. Config-only \u2014 does not create any project data. Run this first, then run `setup` to create your PAPI project.",
@@ -30188,6 +30268,11 @@ async function handleInit(config2, args) {
30188
30268
  await access2(mcpJsonPath);
30189
30269
  existingConfig = await readFile5(mcpJsonPath, "utf-8");
30190
30270
  } catch {
30271
+ }
30272
+ if (existingConfig && isFossilSupabaseReference(extractPapiOwnedConfigText(existingConfig, agent))) {
30273
+ return errorResponse(`${FOSSIL_CONFIG_MESSAGE}
30274
+
30275
+ Path: ${mcpJsonPath}`);
30191
30276
  }
30192
30277
  if (existingConfig && !force) {
30193
30278
  const hasPapiServer = (() => {
@@ -30341,6 +30426,9 @@ ${writeNote}
30341
30426
  ` + formatFilesToWriteSection(collector)
30342
30427
  );
30343
30428
  }
30429
+ if (isDatabaseUser && isFossilSupabaseReference(process.env.DATABASE_URL)) {
30430
+ return errorResponse(FOSSIL_CONFIG_MESSAGE);
30431
+ }
30344
30432
  if (isDatabaseUser) {
30345
30433
  const projectId = randomUUID12();
30346
30434
  const envVars = {
@@ -30379,7 +30467,7 @@ ${writeNote}
30379
30467
  "",
30380
30468
  "## Next Steps",
30381
30469
  "",
30382
- ...process.env.DATABASE_URL ? ["1. **Restart your MCP client** to pick up the new config."] : [`1. **Set your DATABASE_URL** \u2014 replace \`<YOUR_DATABASE_URL>\` in \`${target.displayPath}\` with your Supabase **session pooler** connection string (port **5432**, not 6543 \u2014 the transaction pooler wedges on interrupted calls).`],
30470
+ ...process.env.DATABASE_URL ? ["1. **Restart your MCP client** to pick up the new config."] : [`1. **Set your DATABASE_URL** \u2014 replace \`<YOUR_DATABASE_URL>\` in \`${target.displayPath}\` with the connection string for the self-hosted Postgres/Auth stack you are running (production resolves this over the internal-only VPS tunnel \u2014 see \`infra/vps/self-host-runbook.md\`). This is not a managed Supabase pooler string.`],
30383
30471
  ...process.env.PAPI_USER_ID ? [] : ["2. **Set your PAPI_USER_ID** \u2014 replace `<YOUR_ACCOUNT_UUID>` with your account UUID (getpapi.ai \u2192 Settings \u2192 Account). Release and reviews use it to recognise you as the owner."],
30384
30472
  `${process.env.PAPI_USER_ID ? "2" : "3"}. **Run \`setup\`** \u2014 this scaffolds your project with a Product Brief, Active Decisions, and CLAUDE.md.`
30385
30473
  ].join("\n");
@@ -30427,6 +30515,9 @@ function formatWorkspaceModeLine(result) {
30427
30515
  }
30428
30516
 
30429
30517
  // src/services/health.ts
30518
+ function formatDegradedConnectionLabel() {
30519
+ return getLastAdapterType() === "proxy" ? "connection degraded \u2014 data may be stale. Regenerate your config at https://getpapi.ai if this persists." : "connection degraded \u2014 data may be stale. Check DATABASE_URL in .mcp.json";
30520
+ }
30430
30521
  function computeZoomOutWarning(cycleNumber, lastZoomOutCycle) {
30431
30522
  if (cycleNumber <= 0) return "";
30432
30523
  const baseline = lastZoomOutCycle > 0 ? lastZoomOutCycle : 1;
@@ -31534,7 +31625,7 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
31534
31625
  lines.push("");
31535
31626
  if (health.connectionStatus !== "offline") {
31536
31627
  const statusIcon = health.connectionStatus === "connected" ? "\u2713" : "\u26A0\uFE0F";
31537
- const statusLabel = health.connectionStatus === "connected" ? "Supabase connected" : "Supabase degraded \u2014 data may be stale. Check DATABASE_URL in .mcp.json";
31628
+ const statusLabel = health.connectionStatus === "connected" ? "connected" : formatDegradedConnectionLabel();
31538
31629
  lines.push(`**Connection:** ${statusIcon} ${statusLabel}`);
31539
31630
  lines.push("");
31540
31631
  }
@@ -32691,23 +32782,23 @@ ${formatRuntimeIdentity(getRuntimeIdentity(config2, serverVersion))}`;
32691
32782
  }
32692
32783
  }
32693
32784
  function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, clientName) {
32694
- if (!shouldWriteClaudeMd(clientName)) return "";
32785
+ const targetFile = shouldWriteClaudeMd(clientName) ? "CLAUDE.md" : "AGENTS.md";
32695
32786
  if (adapterType === "proxy") {
32696
32787
  const additions2 = [];
32697
32788
  if (cycleNumber >= 6) additions2.push(CLAUDE_MD_TIER_1);
32698
32789
  if (cycleNumber >= 21) additions2.push(CLAUDE_MD_TIER_2);
32699
32790
  if (additions2.length === 0) return "";
32700
- collector.add({ path: "CLAUDE.md", content: additions2.join(""), mode: "append" });
32791
+ collector.add({ path: targetFile, content: additions2.join(""), mode: "append" });
32701
32792
  const tierNames2 = [];
32702
32793
  if (cycleNumber >= 6) tierNames2.push("Established (batch building, strategy reviews, AD lifecycle)");
32703
32794
  if (cycleNumber >= 21) tierNames2.push("Mature (idea pipeline, doc registry, advanced patterns)");
32704
32795
  return `
32705
32796
 
32706
- \u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
32797
+ \u{1F4DD} **${targetFile} enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
32707
32798
  }
32708
- const claudeMdPath = join18(projectRoot, "CLAUDE.md");
32709
- if (!existsSync11(claudeMdPath)) return "";
32710
- const content = readFileSync13(claudeMdPath, "utf-8");
32799
+ const targetPath = join18(projectRoot, targetFile);
32800
+ if (!existsSync11(targetPath)) return "";
32801
+ const content = readFileSync13(targetPath, "utf-8");
32711
32802
  const additions = [];
32712
32803
  if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
32713
32804
  additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_1));
@@ -32716,13 +32807,13 @@ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector, client
32716
32807
  additions.push(dedupeEnrichmentBlob(content, CLAUDE_MD_TIER_2));
32717
32808
  }
32718
32809
  if (additions.length === 0) return "";
32719
- writeFileSync7(claudeMdPath, content + additions.join(""), "utf-8");
32810
+ writeFileSync7(targetPath, content + additions.join(""), "utf-8");
32720
32811
  const tierNames = [];
32721
32812
  if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1))) tierNames.push("Established (batch building, strategy reviews, AD lifecycle)");
32722
32813
  if (additions.some((a) => a.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T2))) tierNames.push("Mature (idea pipeline, doc registry, advanced patterns)");
32723
32814
  return `
32724
32815
 
32725
- \u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
32816
+ \u{1F4DD} **${targetFile} enriched** \u2014 added ${tierNames.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
32726
32817
  }
32727
32818
 
32728
32819
  // src/tools/hierarchy.ts
@@ -36326,8 +36417,11 @@ function startHttpTransport(opts) {
36326
36417
  const installClient = typeof clientHeader === "string" && KNOWN_INSTALL_CLIENTS.has(clientHeader) ? clientHeader : "direct";
36327
36418
  const projectIdHeader = req.headers["x-papi-project-id"];
36328
36419
  const projectId = typeof projectIdHeader === "string" && projectIdHeader.length > 0 ? projectIdHeader : void 0;
36329
- if (req.method !== "POST" && req.method !== "GET") {
36330
- sendError(res, { status: 405, body: { error: "Method not allowed" } });
36420
+ if (req.method !== "POST") {
36421
+ sendError(res, {
36422
+ status: 405,
36423
+ body: { error: "method_not_allowed", reason: "server does not support server-initiated SSE; use POST" }
36424
+ });
36331
36425
  return;
36332
36426
  }
36333
36427
  const chunks = [];
@@ -36608,6 +36702,49 @@ async function dispatchRequest(args) {
36608
36702
  }
36609
36703
  }
36610
36704
 
36705
+ // src/lib/version-check.ts
36706
+ init_dist();
36707
+ function shouldCheckForUpdate(version, selfHostFlag) {
36708
+ return version !== "unknown" && !isSelfHostedDeployment(selfHostFlag);
36709
+ }
36710
+
36711
+ // src/lib/stdio-idle-timeout.ts
36712
+ var DEFAULT_STDIO_IDLE_TIMEOUT_MS = 12e4;
36713
+ function parseStdioIdleTimeoutMs(raw = process.env["PAPI_STDIO_IDLE_TIMEOUT_MS"]) {
36714
+ if (raw === void 0 || raw.trim() === "") return DEFAULT_STDIO_IDLE_TIMEOUT_MS;
36715
+ const timeoutMs = Number(raw);
36716
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 0) {
36717
+ throw new Error(
36718
+ `PAPI_STDIO_IDLE_TIMEOUT_MS must be a non-negative integer in milliseconds; received ${JSON.stringify(raw)}`
36719
+ );
36720
+ }
36721
+ return timeoutMs;
36722
+ }
36723
+ function startStdioIdleTimeout(input, timeoutMs, onTimeout) {
36724
+ if (timeoutMs === 0) return () => void 0;
36725
+ let timer2;
36726
+ let stopped = false;
36727
+ const stop = () => {
36728
+ if (stopped) return;
36729
+ stopped = true;
36730
+ if (timer2 !== void 0) clearTimeout(timer2);
36731
+ input.off("data", refresh);
36732
+ };
36733
+ const refresh = () => {
36734
+ if (stopped) return;
36735
+ if (timer2 !== void 0) clearTimeout(timer2);
36736
+ timer2 = setTimeout(() => {
36737
+ if (stopped) return;
36738
+ stopped = true;
36739
+ input.off("data", refresh);
36740
+ onTimeout();
36741
+ }, timeoutMs);
36742
+ };
36743
+ input.on("data", refresh);
36744
+ refresh();
36745
+ return stop;
36746
+ }
36747
+
36611
36748
  // src/index.ts
36612
36749
  init_dist();
36613
36750
  var __dirname = dirname7(fileURLToPath5(import.meta.url));
@@ -36709,7 +36846,7 @@ async function gracefulShutdown(signal) {
36709
36846
  } catch (err) {
36710
36847
  console.error(`[papi] Adapter close failed during ${signal}: ${err instanceof Error ? err.message : String(err)}`);
36711
36848
  }
36712
- process.exit(signal === "SIGTERM" || signal === "SIGINT" ? 0 : 1);
36849
+ process.exit(signal === "SIGTERM" || signal === "SIGINT" || signal === "idle-timeout" ? 0 : 1);
36713
36850
  }
36714
36851
  process.on("SIGTERM", () => {
36715
36852
  void gracefulShutdown("SIGTERM");
@@ -36752,7 +36889,7 @@ If you already have an account, check that both **PAPI_PROJECT_ID** and **PAPI_D
36752
36889
  }));
36753
36890
  }
36754
36891
  }
36755
- if (pkgVersion !== "unknown") {
36892
+ if (shouldCheckForUpdate(pkgVersion, process.env["PAPI_SELF_HOST"])) {
36756
36893
  (async () => {
36757
36894
  try {
36758
36895
  const controller = new AbortController();
@@ -36823,13 +36960,22 @@ if (isHttpMode && httpPort !== void 0) {
36823
36960
  } catch {
36824
36961
  }
36825
36962
  const transport = new StdioServerTransport();
36963
+ let stopStdioIdleTimeout = () => void 0;
36964
+ transport.onclose = () => stopStdioIdleTimeout();
36826
36965
  await server.connect(transport);
36966
+ stopStdioIdleTimeout = startStdioIdleTimeout(
36967
+ process.stdin,
36968
+ parseStdioIdleTimeoutMs(),
36969
+ () => {
36970
+ void gracefulShutdown("idle-timeout");
36971
+ }
36972
+ );
36827
36973
  process.stderr.write(`${formatConnectionBanner({
36828
36974
  serverName: config.serverName,
36829
36975
  projectName: basename2(config.projectRoot),
36830
36976
  projectId: config.projectId ?? process.env.PAPI_PROJECT_ID,
36831
36977
  adapterType: config.adapterType,
36832
36978
  pkgVersion
36833
- })}
36979
+ }, getConnectionStatus())}
36834
36980
  `);
36835
36981
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.106",
3
+ "version": "0.7.110",
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
  "mcpName": "io.github.getpapi/papi",