@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 +8 -0
- package/dist/backfill-cycle-metrics.js +13 -1
- package/dist/index.js +206 -60
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
22102
|
-
|
|
22103
|
-
|
|
22104
|
-
|
|
22105
|
-
|
|
22106
|
-
|
|
22107
|
-
|
|
22108
|
-
|
|
22109
|
-
|
|
22110
|
-
}
|
|
22111
|
-
|
|
22112
|
-
|
|
22113
|
-
|
|
22114
|
-
|
|
22115
|
-
|
|
22116
|
-
|
|
22117
|
-
|
|
22118
|
-
|
|
22119
|
-
|
|
22120
|
-
|
|
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
|
-
|
|
22123
|
-
|
|
22124
|
-
|
|
22125
|
-
|
|
22126
|
-
|
|
22127
|
-
|
|
22128
|
-
|
|
22129
|
-
|
|
22130
|
-
|
|
22131
|
-
|
|
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
|
-
${
|
|
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
|
-
|
|
22141
|
-
|
|
22142
|
-
|
|
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
|
|
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" ? "
|
|
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
|
-
|
|
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:
|
|
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}
|
|
32797
|
+
\u{1F4DD} **${targetFile} enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
|
|
32707
32798
|
}
|
|
32708
|
-
const
|
|
32709
|
-
if (!existsSync11(
|
|
32710
|
-
const content = readFileSync13(
|
|
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(
|
|
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}
|
|
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"
|
|
36330
|
-
sendError(res, {
|
|
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
|
|
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.
|
|
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",
|