@papi-ai/server 0.6.2 → 0.7.0
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 +2 -2
- package/dist/index.js +319 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**Takes about 2 minutes.**
|
|
8
8
|
|
|
9
|
-
1. **Get your config** — Sign up at [PAPI](https://
|
|
9
|
+
1. **Get your config** — Sign up at [PAPI](https://getpapi.ai/landing) and copy your `.mcp.json` snippet
|
|
10
10
|
2. **Add to your project** — paste it into `.mcp.json` in your project root
|
|
11
11
|
3. **Restart Claude Code** — it will detect the PAPI server automatically
|
|
12
12
|
4. **In Claude Code:** say `run setup`, then `run plan`
|
|
@@ -31,7 +31,7 @@ That's it. You're planning.
|
|
|
31
31
|
|
|
32
32
|
## Links
|
|
33
33
|
|
|
34
|
-
- **Dashboard** — [PAPI](https://
|
|
34
|
+
- **Dashboard** — [PAPI](https://getpapi.ai/landing)
|
|
35
35
|
- **GitHub** — [cathalos92/papi-ui](https://github.com/cathalos92/papi-ui)
|
|
36
36
|
- **Install guide** — [docs/install-guide.md](https://github.com/cathalos92/papi-ui/blob/main/docs/install-guide.md)
|
|
37
37
|
|
package/dist/index.js
CHANGED
|
@@ -7381,6 +7381,13 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7381
7381
|
UPDATE strategy_recommendations
|
|
7382
7382
|
SET status = 'actioned', actioned_cycle = ${cycleNumber}, updated_at = now()
|
|
7383
7383
|
WHERE id = ${id} AND project_id = ${this.projectId}
|
|
7384
|
+
`;
|
|
7385
|
+
}
|
|
7386
|
+
async dismissRecommendation(id, reason) {
|
|
7387
|
+
await this.sql`
|
|
7388
|
+
UPDATE strategy_recommendations
|
|
7389
|
+
SET status = 'actioned', dismissal_reason = ${reason}, updated_at = now()
|
|
7390
|
+
WHERE id = ${id} AND project_id = ${this.projectId}
|
|
7384
7391
|
`;
|
|
7385
7392
|
}
|
|
7386
7393
|
// -------------------------------------------------------------------------
|
|
@@ -8138,6 +8145,20 @@ var init_proxy_adapter = __esm({
|
|
|
8138
8145
|
} catch {
|
|
8139
8146
|
message = errorBody;
|
|
8140
8147
|
}
|
|
8148
|
+
if (response.status === 401) {
|
|
8149
|
+
throw new Error(
|
|
8150
|
+
`Auth: Invalid API key \u2014 PAPI_DATA_API_KEY was rejected by the proxy.
|
|
8151
|
+
Check PAPI_DATA_API_KEY in your .mcp.json config. You can regenerate it from the PAPI dashboard.
|
|
8152
|
+
(${response.status} on ${method}: ${message})`
|
|
8153
|
+
);
|
|
8154
|
+
}
|
|
8155
|
+
if (response.status === 403 || response.status === 404) {
|
|
8156
|
+
throw new Error(
|
|
8157
|
+
`Auth: Project not found or access denied \u2014 PAPI_PROJECT_ID may be wrong.
|
|
8158
|
+
Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI dashboard settings.
|
|
8159
|
+
(${response.status} on ${method}: ${message})`
|
|
8160
|
+
);
|
|
8161
|
+
}
|
|
8141
8162
|
throw new Error(`Proxy error (${response.status}) on ${method}: ${message}`);
|
|
8142
8163
|
}
|
|
8143
8164
|
const body = await response.json();
|
|
@@ -8161,6 +8182,27 @@ var init_proxy_adapter = __esm({
|
|
|
8161
8182
|
return false;
|
|
8162
8183
|
}
|
|
8163
8184
|
}
|
|
8185
|
+
/**
|
|
8186
|
+
* Validate API key and project access against the proxy.
|
|
8187
|
+
* Returns HTTP status so callers can distinguish 401 (bad key) from 403/404 (bad project).
|
|
8188
|
+
* Status 0 means a network error occurred.
|
|
8189
|
+
*/
|
|
8190
|
+
async probeAuth(projectId) {
|
|
8191
|
+
try {
|
|
8192
|
+
const response = await fetch(`${this.endpoint}/invoke`, {
|
|
8193
|
+
method: "POST",
|
|
8194
|
+
headers: {
|
|
8195
|
+
"Content-Type": "application/json",
|
|
8196
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
8197
|
+
},
|
|
8198
|
+
body: JSON.stringify({ projectId, method: "projectExists", args: [] }),
|
|
8199
|
+
signal: AbortSignal.timeout(5e3)
|
|
8200
|
+
});
|
|
8201
|
+
return { ok: response.ok, status: response.status };
|
|
8202
|
+
} catch {
|
|
8203
|
+
return { ok: false, status: 0 };
|
|
8204
|
+
}
|
|
8205
|
+
}
|
|
8164
8206
|
// --- Planning & Health ---
|
|
8165
8207
|
readPlanningLog() {
|
|
8166
8208
|
return this.invoke("readPlanningLog");
|
|
@@ -8411,6 +8453,9 @@ var init_proxy_adapter = __esm({
|
|
|
8411
8453
|
projectExists() {
|
|
8412
8454
|
return this.invoke("projectExists");
|
|
8413
8455
|
}
|
|
8456
|
+
getBuildReportCountForTask(taskId) {
|
|
8457
|
+
return this.invoke("getBuildReportCountForTask", [taskId]);
|
|
8458
|
+
}
|
|
8414
8459
|
// --- Atomic plan write-back ---
|
|
8415
8460
|
planWriteBack(payload) {
|
|
8416
8461
|
return this.invoke("planWriteBack", [payload]);
|
|
@@ -8919,7 +8964,15 @@ var init_git = __esm({
|
|
|
8919
8964
|
});
|
|
8920
8965
|
|
|
8921
8966
|
// src/index.ts
|
|
8967
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
8968
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
8969
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8922
8970
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8971
|
+
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8972
|
+
import {
|
|
8973
|
+
CallToolRequestSchema as CallToolRequestSchema2,
|
|
8974
|
+
ListToolsRequestSchema as ListToolsRequestSchema2
|
|
8975
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8923
8976
|
|
|
8924
8977
|
// src/config.ts
|
|
8925
8978
|
import path from "path";
|
|
@@ -9067,7 +9120,7 @@ async function createAdapter(optionsOrType, maybePapiDir) {
|
|
|
9067
9120
|
}
|
|
9068
9121
|
case "proxy": {
|
|
9069
9122
|
const { ProxyPapiAdapter: ProxyPapiAdapter2 } = await Promise.resolve().then(() => (init_proxy_adapter(), proxy_adapter_exports));
|
|
9070
|
-
const dashboardUrl = process.env["PAPI_DASHBOARD_URL"] || "https://
|
|
9123
|
+
const dashboardUrl = process.env["PAPI_DASHBOARD_URL"] || "https://getpapi.ai";
|
|
9071
9124
|
const projectId = process.env["PAPI_PROJECT_ID"];
|
|
9072
9125
|
const dataApiKey = process.env["PAPI_DATA_API_KEY"];
|
|
9073
9126
|
if (!dataApiKey) {
|
|
@@ -9089,13 +9142,33 @@ Already have an account? Make sure PAPI_DATA_API_KEY is set in your .mcp.json en
|
|
|
9089
9142
|
projectId: projectId || void 0
|
|
9090
9143
|
});
|
|
9091
9144
|
const connected = await adapter2.probeConnection();
|
|
9092
|
-
if (connected) {
|
|
9093
|
-
_connectionStatus = "connected";
|
|
9094
|
-
console.error("[papi] \u2713 Data proxy connected");
|
|
9095
|
-
} else {
|
|
9145
|
+
if (!connected) {
|
|
9096
9146
|
_connectionStatus = "degraded";
|
|
9097
9147
|
console.error("[papi] \u2717 Data proxy unreachable \u2014 running in degraded mode");
|
|
9098
9148
|
console.error("[papi] Check your PAPI_DATA_ENDPOINT configuration.");
|
|
9149
|
+
} else if (projectId) {
|
|
9150
|
+
const auth = await adapter2.probeAuth(projectId);
|
|
9151
|
+
if (!auth.ok) {
|
|
9152
|
+
if (auth.status === 401) {
|
|
9153
|
+
throw new Error(
|
|
9154
|
+
"PAPI_DATA_API_KEY is invalid \u2014 authentication failed.\nCheck the key in your .mcp.json config. You can generate a new key from the PAPI dashboard."
|
|
9155
|
+
);
|
|
9156
|
+
} else if (auth.status === 403 || auth.status === 404) {
|
|
9157
|
+
throw new Error(
|
|
9158
|
+
`PAPI_PROJECT_ID "${projectId}" was not found or you don't have access.
|
|
9159
|
+
Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI dashboard settings.`
|
|
9160
|
+
);
|
|
9161
|
+
} else if (auth.status !== 0) {
|
|
9162
|
+
_connectionStatus = "degraded";
|
|
9163
|
+
console.error(`[papi] \u26A0 Auth check returned ${auth.status} \u2014 running in degraded mode`);
|
|
9164
|
+
}
|
|
9165
|
+
} else {
|
|
9166
|
+
_connectionStatus = "connected";
|
|
9167
|
+
console.error("[papi] \u2713 Data proxy connected");
|
|
9168
|
+
}
|
|
9169
|
+
} else {
|
|
9170
|
+
_connectionStatus = "connected";
|
|
9171
|
+
console.error("[papi] \u2713 Data proxy reachable \u2014 project will be auto-provisioned");
|
|
9099
9172
|
}
|
|
9100
9173
|
if (!projectId && connected) {
|
|
9101
9174
|
try {
|
|
@@ -11691,7 +11764,7 @@ ${cleanContent}`;
|
|
|
11691
11764
|
pendingRecIds = pending.map((r) => r.id);
|
|
11692
11765
|
} catch {
|
|
11693
11766
|
}
|
|
11694
|
-
const
|
|
11767
|
+
const handoffs = (data.cycleHandoffs ?? []).map((h) => {
|
|
11695
11768
|
const parsed = parseBuildHandoff(h.buildHandoff);
|
|
11696
11769
|
if (parsed && !parsed.createdAt) {
|
|
11697
11770
|
parsed.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -11730,8 +11803,8 @@ ${cleanContent}`;
|
|
|
11730
11803
|
} catch {
|
|
11731
11804
|
}
|
|
11732
11805
|
const effortMapLegacy = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
|
|
11733
|
-
const legacyTaskCount =
|
|
11734
|
-
const legacyEffortPoints =
|
|
11806
|
+
const legacyTaskCount = handoffs.length;
|
|
11807
|
+
const legacyEffortPoints = handoffs.reduce((sum, h) => sum + (effortMapLegacy[h.handoff.effort] ?? 3), 0);
|
|
11735
11808
|
const payload = {
|
|
11736
11809
|
cycleNumber: newCycleNumber,
|
|
11737
11810
|
cycleLog: {
|
|
@@ -11781,7 +11854,7 @@ ${cleanContent}`;
|
|
|
11781
11854
|
body: ad.body
|
|
11782
11855
|
})),
|
|
11783
11856
|
pendingRecommendationIds: pendingRecIds,
|
|
11784
|
-
handoffs
|
|
11857
|
+
handoffs,
|
|
11785
11858
|
cycle,
|
|
11786
11859
|
reviewedTaskIds
|
|
11787
11860
|
};
|
|
@@ -11830,8 +11903,12 @@ async function writeBack(adapter2, _mode, cycleNumber, data, contextHashes) {
|
|
|
11830
11903
|
|
|
11831
11904
|
${cleanContent}`;
|
|
11832
11905
|
const effortMap = { XS: 1, S: 2, M: 3, L: 5, XL: 8 };
|
|
11833
|
-
const
|
|
11834
|
-
|
|
11906
|
+
const legacyHandoffs = (data.cycleHandoffs ?? []).map((h) => {
|
|
11907
|
+
const parsed = parseBuildHandoff(h.buildHandoff);
|
|
11908
|
+
return { taskId: h.taskId, handoff: parsed ?? { effort: "M" } };
|
|
11909
|
+
});
|
|
11910
|
+
const cycleTaskCount = legacyHandoffs.length;
|
|
11911
|
+
const cycleEffortPoints = legacyHandoffs.reduce((sum, h) => sum + (effortMap[h.handoff.effort] ?? 3), 0);
|
|
11835
11912
|
const cycleLogPromise = adapter2.writeCycleLogEntry({
|
|
11836
11913
|
uuid: randomUUID7(),
|
|
11837
11914
|
cycleNumber: newCycleNumber,
|
|
@@ -12233,7 +12310,7 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
12233
12310
|
const [decisions, reports, brief] = await Promise.all([
|
|
12234
12311
|
adapter2.getActiveDecisions(),
|
|
12235
12312
|
adapter2.getRecentBuildReports(10),
|
|
12236
|
-
adapter2.
|
|
12313
|
+
adapter2.readProductBrief()
|
|
12237
12314
|
]);
|
|
12238
12315
|
const northStar = await adapter2.getCurrentNorthStar?.() ?? "";
|
|
12239
12316
|
const userMessage2 = buildHandoffsOnlyUserMessage({
|
|
@@ -12971,6 +13048,27 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
12971
13048
|
adapter2.searchDocs?.({ status: "active", limit: 10 })?.catch(() => []) ?? Promise.resolve([])
|
|
12972
13049
|
]);
|
|
12973
13050
|
const tasks = [...activeTasks, ...recentDoneTasks];
|
|
13051
|
+
const existingAdIds = new Set(decisions.map((d) => d.id));
|
|
13052
|
+
const survivingPendingRecs = [];
|
|
13053
|
+
for (const rec of pendingRecs) {
|
|
13054
|
+
const isOrphaned = (() => {
|
|
13055
|
+
if (rec.target && /^AD-\d+$/.test(rec.target) && !existingAdIds.has(rec.target)) return true;
|
|
13056
|
+
if (rec.type === "ad_update") {
|
|
13057
|
+
const adMatch = rec.content.match(/^(?:delete|resolve|confidence_change|supersede|modify):\s*(AD-\d+)/i);
|
|
13058
|
+
if (adMatch && !existingAdIds.has(adMatch[1])) return true;
|
|
13059
|
+
}
|
|
13060
|
+
return false;
|
|
13061
|
+
})();
|
|
13062
|
+
if (isOrphaned) {
|
|
13063
|
+
try {
|
|
13064
|
+
await adapter2.dismissRecommendation?.(rec.id, "target AD no longer exists");
|
|
13065
|
+
console.error(`[strategy_review] Swept orphaned pending rec ${rec.id} (target: ${rec.target ?? rec.content.slice(0, 50)})`);
|
|
13066
|
+
} catch {
|
|
13067
|
+
}
|
|
13068
|
+
} else {
|
|
13069
|
+
survivingPendingRecs.push(rec);
|
|
13070
|
+
}
|
|
13071
|
+
}
|
|
12974
13072
|
const recentLog = log;
|
|
12975
13073
|
let buildPatternsText;
|
|
12976
13074
|
let reviewPatternsText;
|
|
@@ -13076,7 +13174,7 @@ ${lines.join("\n")}`;
|
|
|
13076
13174
|
try {
|
|
13077
13175
|
const plansDir = join2(homedir(), ".claude", "plans");
|
|
13078
13176
|
if (existsSync(plansDir)) {
|
|
13079
|
-
const lastReviewDate = previousStrategyReviews?.[0]?.
|
|
13177
|
+
const lastReviewDate = previousStrategyReviews?.[0]?.createdAt ? new Date(previousStrategyReviews[0].createdAt) : /* @__PURE__ */ new Date(0);
|
|
13080
13178
|
const planFiles = readdirSync(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
13081
13179
|
const fullPath = join2(plansDir, f);
|
|
13082
13180
|
const stat2 = statSync(fullPath);
|
|
@@ -13320,7 +13418,7 @@ ${cleanContent}`;
|
|
|
13320
13418
|
} else {
|
|
13321
13419
|
await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber, ad.action);
|
|
13322
13420
|
}
|
|
13323
|
-
const eventType = ad.action === "delete" ? "
|
|
13421
|
+
const eventType = ad.action === "delete" ? "invalidated" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
|
|
13324
13422
|
try {
|
|
13325
13423
|
await adapter2.appendDecisionEvent({
|
|
13326
13424
|
decisionId: ad.id,
|
|
@@ -13381,7 +13479,25 @@ ${cleanContent}`;
|
|
|
13381
13479
|
try {
|
|
13382
13480
|
const recs = extractRecommendations(data, cycleNumber);
|
|
13383
13481
|
if (recs.length > 0) {
|
|
13384
|
-
await
|
|
13482
|
+
const existingAds = await adapter2.getActiveDecisions().catch(() => []);
|
|
13483
|
+
const existingAdIds = new Set(existingAds.map((ad) => ad.id));
|
|
13484
|
+
const filteredRecs = recs.filter((rec) => {
|
|
13485
|
+
if (rec.target && /^AD-\d+$/.test(rec.target)) {
|
|
13486
|
+
if (!existingAdIds.has(rec.target)) {
|
|
13487
|
+
console.error(`[strategy_review] Skipped ghost recommendation for non-existent ${rec.target}: "${rec.content.slice(0, 80)}"`);
|
|
13488
|
+
return false;
|
|
13489
|
+
}
|
|
13490
|
+
}
|
|
13491
|
+
if (rec.type === "ad_update") {
|
|
13492
|
+
const adMatch = rec.content.match(/^(?:delete|resolve|confidence_change|supersede|modify):\s*(AD-\d+)/i);
|
|
13493
|
+
if (adMatch && !existingAdIds.has(adMatch[1])) {
|
|
13494
|
+
console.error(`[strategy_review] Skipped ghost recommendation for non-existent ${adMatch[1]}: "${rec.content.slice(0, 80)}"`);
|
|
13495
|
+
return false;
|
|
13496
|
+
}
|
|
13497
|
+
}
|
|
13498
|
+
return true;
|
|
13499
|
+
});
|
|
13500
|
+
await Promise.all(filteredRecs.map((rec) => adapter2.writeRecommendation(rec)));
|
|
13385
13501
|
}
|
|
13386
13502
|
} catch {
|
|
13387
13503
|
}
|
|
@@ -13463,6 +13579,17 @@ async function processReviewOutput(adapter2, rawOutput, cycleNumber) {
|
|
|
13463
13579
|
let slackWarning;
|
|
13464
13580
|
let writeBackFailed;
|
|
13465
13581
|
let phaseChanges;
|
|
13582
|
+
if (!data) {
|
|
13583
|
+
const marker = "<!-- PAPI_STRUCTURED_OUTPUT -->";
|
|
13584
|
+
const hasMarker = rawOutput.includes(marker);
|
|
13585
|
+
if (hasMarker) {
|
|
13586
|
+
const afterMarker = rawOutput.slice(rawOutput.indexOf(marker) + marker.length, rawOutput.indexOf(marker) + marker.length + 300).trim();
|
|
13587
|
+
writeBackFailed = `Structured output marker found but JSON block was missing or malformed. Content after marker (first 300 chars): "${afterMarker}". Ensure your output includes a valid \`\`\`json block after <!-- PAPI_STRUCTURED_OUTPUT -->.`;
|
|
13588
|
+
} else {
|
|
13589
|
+
const preview = rawOutput.slice(0, 200).trim();
|
|
13590
|
+
writeBackFailed = `No structured output marker found. Expected "<!-- PAPI_STRUCTURED_OUTPUT -->" followed by a \`\`\`json block. Received (first 200 chars): "${preview}". Re-run strategy_review apply with the complete output including both parts.`;
|
|
13591
|
+
}
|
|
13592
|
+
}
|
|
13466
13593
|
if (data) {
|
|
13467
13594
|
try {
|
|
13468
13595
|
phaseChanges = await writeBack2(adapter2, cycleNumber, data, displayText);
|
|
@@ -13686,8 +13813,8 @@ async function formatHierarchyForReview(adapter2, currentCycle, prefetchedTasks)
|
|
|
13686
13813
|
let phases = [];
|
|
13687
13814
|
try {
|
|
13688
13815
|
[horizons, stages, phases] = await Promise.all([
|
|
13689
|
-
adapter2.readHorizons(),
|
|
13690
|
-
adapter2.readStages(),
|
|
13816
|
+
adapter2.readHorizons?.() ?? [],
|
|
13817
|
+
adapter2.readStages?.() ?? [],
|
|
13691
13818
|
adapter2.readPhases()
|
|
13692
13819
|
]);
|
|
13693
13820
|
} catch {
|
|
@@ -13820,7 +13947,7 @@ ${cleanContent}`;
|
|
|
13820
13947
|
} else {
|
|
13821
13948
|
await adapter2.updateActiveDecision(ad.id, ad.body, cycleNumber, ad.action);
|
|
13822
13949
|
}
|
|
13823
|
-
const eventType = ad.action === "delete" ? "
|
|
13950
|
+
const eventType = ad.action === "delete" ? "invalidated" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
|
|
13824
13951
|
try {
|
|
13825
13952
|
await adapter2.appendDecisionEvent({
|
|
13826
13953
|
decisionId: ad.id,
|
|
@@ -13933,7 +14060,7 @@ async function captureDecision(adapter2, input) {
|
|
|
13933
14060
|
decisionId: adId,
|
|
13934
14061
|
eventType: adAction === "created" ? "created" : "modified",
|
|
13935
14062
|
cycle: cycleNumber,
|
|
13936
|
-
source: "
|
|
14063
|
+
source: "strategy_change",
|
|
13937
14064
|
sourceRef: `cycle-${cycleNumber}-capture`,
|
|
13938
14065
|
detail: `Captured: ${input.text.slice(0, 200)}`
|
|
13939
14066
|
});
|
|
@@ -14434,10 +14561,10 @@ function formatBoard(result) {
|
|
|
14434
14561
|
t.title,
|
|
14435
14562
|
t.status,
|
|
14436
14563
|
t.cycle != null ? String(t.cycle) : "-",
|
|
14437
|
-
t.phase,
|
|
14438
|
-
t.module,
|
|
14439
|
-
t.epic,
|
|
14440
|
-
t.complexity,
|
|
14564
|
+
t.phase ?? "-",
|
|
14565
|
+
t.module ?? "-",
|
|
14566
|
+
t.epic ?? "-",
|
|
14567
|
+
t.complexity ?? "-",
|
|
14441
14568
|
t.createdAt ?? "-"
|
|
14442
14569
|
]);
|
|
14443
14570
|
const widths = headers.map(
|
|
@@ -17828,15 +17955,15 @@ async function prepareReconcile(adapter2) {
|
|
|
17828
17955
|
const misaligned = allTasks.filter((t) => {
|
|
17829
17956
|
const mapping = phaseStageMap.get(t.phase ?? "");
|
|
17830
17957
|
if (!mapping) return false;
|
|
17831
|
-
return mapping.stage.status === "
|
|
17958
|
+
return mapping.stage.status === "deferred" || mapping.horizon.status === "deferred";
|
|
17832
17959
|
});
|
|
17833
17960
|
if (misaligned.length > 0) {
|
|
17834
|
-
lines.push("### Hierarchy Misalignment (tasks in
|
|
17961
|
+
lines.push("### Hierarchy Misalignment (tasks in deferred stages/horizons)");
|
|
17835
17962
|
for (const t of misaligned) {
|
|
17836
17963
|
const mapping = phaseStageMap.get(t.phase ?? "");
|
|
17837
17964
|
const stageLabel = mapping?.stage.label ?? "unknown";
|
|
17838
17965
|
const horizonLabel = mapping?.horizon.label ?? "unknown";
|
|
17839
|
-
lines.push(`- **${t.id}:** ${t.title} \u2014 phase "${t.phase}" belongs to **${stageLabel}** (${horizonLabel}), which is
|
|
17966
|
+
lines.push(`- **${t.id}:** ${t.title} \u2014 phase "${t.phase}" belongs to **${stageLabel}** (${horizonLabel}), which is deferred. Consider deferring or reassigning.`);
|
|
17840
17967
|
}
|
|
17841
17968
|
lines.push("");
|
|
17842
17969
|
}
|
|
@@ -18006,7 +18133,7 @@ async function prepareRetriage(adapter2) {
|
|
|
18006
18133
|
lines.push(`**${allTasks.length} tasks** to reassess priority and complexity.`);
|
|
18007
18134
|
lines.push("");
|
|
18008
18135
|
try {
|
|
18009
|
-
const ads = await adapter2.
|
|
18136
|
+
const ads = await adapter2.getActiveDecisions();
|
|
18010
18137
|
if (ads.length > 0) {
|
|
18011
18138
|
lines.push("### Active Decisions (strategic context)");
|
|
18012
18139
|
for (const ad of ads.slice(0, 10)) {
|
|
@@ -19368,37 +19495,84 @@ Path: ${mcpJsonPath}`
|
|
|
19368
19495
|
}
|
|
19369
19496
|
}
|
|
19370
19497
|
}
|
|
19371
|
-
const
|
|
19372
|
-
const
|
|
19373
|
-
|
|
19374
|
-
|
|
19375
|
-
|
|
19376
|
-
|
|
19377
|
-
|
|
19378
|
-
|
|
19379
|
-
|
|
19380
|
-
|
|
19381
|
-
|
|
19498
|
+
const existingApiKey = process.env.PAPI_DATA_API_KEY;
|
|
19499
|
+
const existingProjectId = process.env.PAPI_PROJECT_ID;
|
|
19500
|
+
const isProxyUser = Boolean(existingApiKey) || config2.adapterType === "proxy";
|
|
19501
|
+
const isDatabaseUser = Boolean(process.env.DATABASE_URL) || config2.adapterType === "pg";
|
|
19502
|
+
if (isProxyUser && existingApiKey && existingProjectId) {
|
|
19503
|
+
const mcpConfig = {
|
|
19504
|
+
mcpServers: {
|
|
19505
|
+
papi: {
|
|
19506
|
+
command: "npx",
|
|
19507
|
+
args: ["-y", "@papi-ai/server"],
|
|
19508
|
+
env: {
|
|
19509
|
+
PAPI_PROJECT_ID: existingProjectId,
|
|
19510
|
+
PAPI_DATA_API_KEY: existingApiKey
|
|
19511
|
+
}
|
|
19382
19512
|
}
|
|
19383
19513
|
}
|
|
19384
|
-
}
|
|
19385
|
-
|
|
19386
|
-
|
|
19387
|
-
|
|
19514
|
+
};
|
|
19515
|
+
await writeFile4(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
19516
|
+
await ensureGitignoreEntry(projectRoot, ".mcp.json");
|
|
19517
|
+
return textResponse(
|
|
19518
|
+
`# PAPI Initialised \u2014 ${projectName}
|
|
19519
|
+
|
|
19520
|
+
**Config:** \`${mcpJsonPath}\`
|
|
19521
|
+
|
|
19522
|
+
Your existing API key and project ID have been saved to .mcp.json.
|
|
19523
|
+
|
|
19524
|
+
## Next Steps
|
|
19525
|
+
|
|
19526
|
+
1. **Restart your MCP client** to pick up the new config.
|
|
19527
|
+
2. **Run \`setup\`** \u2014 this scaffolds your project with a Product Brief and CLAUDE.md.
|
|
19528
|
+
`
|
|
19529
|
+
);
|
|
19530
|
+
}
|
|
19531
|
+
if (isDatabaseUser) {
|
|
19532
|
+
const projectId = randomUUID14();
|
|
19533
|
+
const mcpConfig = {
|
|
19534
|
+
mcpServers: {
|
|
19535
|
+
papi: {
|
|
19536
|
+
command: "npx",
|
|
19537
|
+
args: ["-y", "@papi-ai/server"],
|
|
19538
|
+
env: {
|
|
19539
|
+
PAPI_PROJECT_DIR: projectRoot,
|
|
19540
|
+
PAPI_ADAPTER: "pg",
|
|
19541
|
+
DATABASE_URL: process.env.DATABASE_URL || "<YOUR_DATABASE_URL>",
|
|
19542
|
+
PAPI_PROJECT_ID: projectId
|
|
19543
|
+
}
|
|
19544
|
+
}
|
|
19545
|
+
}
|
|
19546
|
+
};
|
|
19547
|
+
await writeFile4(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
19548
|
+
await ensureGitignoreEntry(projectRoot, ".mcp.json");
|
|
19549
|
+
const output2 = [
|
|
19550
|
+
`# PAPI Initialised \u2014 ${projectName}`,
|
|
19551
|
+
"",
|
|
19552
|
+
`**Project ID:** \`${projectId}\``,
|
|
19553
|
+
`**Config:** \`${mcpJsonPath}\``,
|
|
19554
|
+
"",
|
|
19555
|
+
"## Next Steps",
|
|
19556
|
+
"",
|
|
19557
|
+
...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 `.mcp.json` with your Supabase connection string."],
|
|
19558
|
+
"2. **Run `setup`** \u2014 this scaffolds your project with a Product Brief, Active Decisions, and CLAUDE.md."
|
|
19559
|
+
].join("\n");
|
|
19560
|
+
return textResponse(output2);
|
|
19561
|
+
}
|
|
19388
19562
|
const output = [
|
|
19389
|
-
`# PAPI
|
|
19563
|
+
`# PAPI \u2014 Account Required`,
|
|
19564
|
+
"",
|
|
19565
|
+
`PAPI needs an account to store your project data.`,
|
|
19390
19566
|
"",
|
|
19391
|
-
|
|
19392
|
-
`**Project directory:** \`${projectRoot}\`${usingCwdDefault ? " *(detected from current directory)*" : ""}`,
|
|
19393
|
-
`**Config:** \`${mcpJsonPath}\``,
|
|
19567
|
+
"## Get Started in 3 Steps",
|
|
19394
19568
|
"",
|
|
19395
|
-
"
|
|
19569
|
+
"1. **Sign up** at https://getpapi.ai/login",
|
|
19570
|
+
"2. **Complete the onboarding wizard** \u2014 it generates your `.mcp.json` config with your API key and project ID",
|
|
19571
|
+
"3. **Download the config**, place it in your project root, and restart your MCP client",
|
|
19396
19572
|
"",
|
|
19397
|
-
"
|
|
19398
|
-
"2. **Restart Claude Code** \u2014 it will detect the new MCP server on restart.",
|
|
19399
|
-
"3. **Run `setup`** \u2014 this scaffolds your project with a Product Brief, Active Decisions, and CLAUDE.md.",
|
|
19573
|
+
"The onboarding wizard generates everything you need \u2014 no manual configuration required.",
|
|
19400
19574
|
"",
|
|
19401
|
-
|
|
19575
|
+
`> Already have an account? Make sure both \`PAPI_PROJECT_ID\` and \`PAPI_DATA_API_KEY\` are set in your .mcp.json.`
|
|
19402
19576
|
].join("\n");
|
|
19403
19577
|
return textResponse(output);
|
|
19404
19578
|
}
|
|
@@ -19536,15 +19710,15 @@ function formatOrientSummary(health, buildInfo, hierarchy, latestTag, projectRoo
|
|
|
19536
19710
|
async function getHierarchyPosition(adapter2) {
|
|
19537
19711
|
try {
|
|
19538
19712
|
const [horizons, stages, phases, allTasks] = await Promise.all([
|
|
19539
|
-
adapter2.readHorizons(),
|
|
19540
|
-
adapter2.readStages(),
|
|
19713
|
+
adapter2.readHorizons?.() ?? [],
|
|
19714
|
+
adapter2.readStages?.() ?? [],
|
|
19541
19715
|
adapter2.readPhases(),
|
|
19542
19716
|
adapter2.queryBoard()
|
|
19543
19717
|
]);
|
|
19544
19718
|
if (horizons.length === 0) return void 0;
|
|
19545
|
-
const activeHorizon = horizons.find((h) => h.status === "
|
|
19719
|
+
const activeHorizon = horizons.find((h) => h.status === "active") || horizons[0];
|
|
19546
19720
|
const activeStages = stages.filter((s) => s.horizonId === activeHorizon.id);
|
|
19547
|
-
const activeStage = activeStages.find((s) => s.status === "
|
|
19721
|
+
const activeStage = activeStages.find((s) => s.status === "active") || activeStages[0];
|
|
19548
19722
|
if (!activeStage) return void 0;
|
|
19549
19723
|
const stagePhases = phases.filter((p) => p.stageId === activeStage.id);
|
|
19550
19724
|
const activePhases = stagePhases.filter((p) => p.status === "In Progress");
|
|
@@ -19814,6 +19988,9 @@ async function handleHierarchyUpdate(adapter2, args) {
|
|
|
19814
19988
|
const available = phases.map((p) => p.label).join(", ");
|
|
19815
19989
|
return errorResponse(`Phase "${name}" not found. Available phases: ${available || "none"}`);
|
|
19816
19990
|
}
|
|
19991
|
+
if (!status) {
|
|
19992
|
+
return errorResponse("status is required for phase updates.");
|
|
19993
|
+
}
|
|
19817
19994
|
if (phase.status === status) {
|
|
19818
19995
|
return textResponse(`Phase "${phase.label}" is already "${status}". No change made.`);
|
|
19819
19996
|
}
|
|
@@ -19867,6 +20044,9 @@ async function handleHierarchyUpdate(adapter2, args) {
|
|
|
19867
20044
|
const available = horizons.map((h) => h.label).join(", ");
|
|
19868
20045
|
return errorResponse(`Horizon "${name}" not found. Available horizons: ${available || "none"}`);
|
|
19869
20046
|
}
|
|
20047
|
+
if (!status) {
|
|
20048
|
+
return errorResponse("status is required for horizon updates.");
|
|
20049
|
+
}
|
|
19870
20050
|
if (horizon.status === status) {
|
|
19871
20051
|
return textResponse(`Horizon "${horizon.label}" is already "${status}". No change made.`);
|
|
19872
20052
|
}
|
|
@@ -19930,8 +20110,8 @@ async function assembleZoomOutContext(adapter2, cycleNumber, projectRoot) {
|
|
|
19930
20110
|
let hierarchyText = "";
|
|
19931
20111
|
try {
|
|
19932
20112
|
const [horizons, stages, phases] = await Promise.all([
|
|
19933
|
-
adapter2.readHorizons(),
|
|
19934
|
-
adapter2.readStages(),
|
|
20113
|
+
adapter2.readHorizons?.() ?? [],
|
|
20114
|
+
adapter2.readStages?.() ?? [],
|
|
19935
20115
|
adapter2.readPhases()
|
|
19936
20116
|
]);
|
|
19937
20117
|
if (horizons.length > 0) {
|
|
@@ -20579,8 +20759,8 @@ function createServer(adapter2, config2) {
|
|
|
20579
20759
|
{ capabilities: { tools: {}, prompts: {} } }
|
|
20580
20760
|
);
|
|
20581
20761
|
const __filename = fileURLToPath(import.meta.url);
|
|
20582
|
-
const
|
|
20583
|
-
const skillsDir = join10(
|
|
20762
|
+
const __dirname2 = dirname(__filename);
|
|
20763
|
+
const skillsDir = join10(__dirname2, "..", "skills");
|
|
20584
20764
|
function parseSkillFrontmatter(content) {
|
|
20585
20765
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
20586
20766
|
if (!match) return null;
|
|
@@ -20681,7 +20861,7 @@ function createServer(adapter2, config2) {
|
|
|
20681
20861
|
return {
|
|
20682
20862
|
content: [{
|
|
20683
20863
|
type: "text",
|
|
20684
|
-
text: `No project found for PAPI_PROJECT_ID \`${
|
|
20864
|
+
text: `No project found for PAPI_PROJECT_ID \`${process.env.PAPI_PROJECT_ID ?? "(not set)"}\`. Run \`setup\` first to initialise your project.`
|
|
20685
20865
|
}]
|
|
20686
20866
|
};
|
|
20687
20867
|
}
|
|
@@ -20815,15 +20995,88 @@ function createServer(adapter2, config2) {
|
|
|
20815
20995
|
}
|
|
20816
20996
|
|
|
20817
20997
|
// src/index.ts
|
|
20998
|
+
var __dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
20999
|
+
var pkgVersion = "unknown";
|
|
21000
|
+
try {
|
|
21001
|
+
const pkg = JSON.parse(readFileSync4(join11(__dirname, "..", "package.json"), "utf-8"));
|
|
21002
|
+
pkgVersion = pkg.version;
|
|
21003
|
+
} catch {
|
|
21004
|
+
}
|
|
21005
|
+
var cliArgs = process.argv.slice(2);
|
|
21006
|
+
if (cliArgs.includes("--help") || cliArgs.includes("-h")) {
|
|
21007
|
+
console.log(`papi-server v${pkgVersion} \u2014 PAPI MCP server
|
|
21008
|
+
|
|
21009
|
+
Usage: npx @papi-ai/server [options]
|
|
21010
|
+
|
|
21011
|
+
Options:
|
|
21012
|
+
--help, -h Show this help message
|
|
21013
|
+
--version, -v Show version number
|
|
21014
|
+
--project <dir> Set the project directory
|
|
21015
|
+
|
|
21016
|
+
Getting started:
|
|
21017
|
+
1. Sign up at https://getpapi.ai/login
|
|
21018
|
+
2. Complete the onboarding wizard to get your API key
|
|
21019
|
+
3. Add the generated config to your MCP client
|
|
21020
|
+
4. Say "run setup" in your AI tool
|
|
21021
|
+
|
|
21022
|
+
Docs: https://getpapi.ai/docs
|
|
21023
|
+
`);
|
|
21024
|
+
process.exit(0);
|
|
21025
|
+
}
|
|
21026
|
+
if (cliArgs.includes("--version") || cliArgs.includes("-v")) {
|
|
21027
|
+
console.log(pkgVersion);
|
|
21028
|
+
process.exit(0);
|
|
21029
|
+
}
|
|
20818
21030
|
process.on("unhandledRejection", (err) => {
|
|
20819
21031
|
console.error("[papi] unhandledRejection (swallowed):", err instanceof Error ? err.message : err);
|
|
20820
21032
|
});
|
|
20821
21033
|
var config = loadConfig();
|
|
20822
|
-
var adapter
|
|
20823
|
-
|
|
20824
|
-
|
|
20825
|
-
|
|
20826
|
-
|
|
20827
|
-
|
|
21034
|
+
var adapter;
|
|
21035
|
+
var setupError;
|
|
21036
|
+
try {
|
|
21037
|
+
adapter = await createAdapter({
|
|
21038
|
+
adapterType: config.adapterType,
|
|
21039
|
+
papiDir: config.papiDir,
|
|
21040
|
+
papiEndpoint: config.papiEndpoint
|
|
21041
|
+
});
|
|
21042
|
+
} catch (err) {
|
|
21043
|
+
setupError = err instanceof Error ? err.message : String(err);
|
|
21044
|
+
process.stderr.write(`[papi] Startup error: ${setupError}
|
|
21045
|
+
`);
|
|
21046
|
+
}
|
|
21047
|
+
var server;
|
|
21048
|
+
if (adapter && !setupError) {
|
|
21049
|
+
server = createServer(adapter, config);
|
|
21050
|
+
} else {
|
|
21051
|
+
server = new Server2(
|
|
21052
|
+
{ name: "papi", version: pkgVersion },
|
|
21053
|
+
{ capabilities: { tools: {} } }
|
|
21054
|
+
);
|
|
21055
|
+
const errorMessage = setupError || "Unknown startup error";
|
|
21056
|
+
server.setRequestHandler(ListToolsRequestSchema2, async () => ({
|
|
21057
|
+
tools: [{
|
|
21058
|
+
name: "setup",
|
|
21059
|
+
description: "PAPI is not connected \u2014 run this tool for setup instructions.",
|
|
21060
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
21061
|
+
}]
|
|
21062
|
+
}));
|
|
21063
|
+
server.setRequestHandler(CallToolRequestSchema2, async () => ({
|
|
21064
|
+
content: [{
|
|
21065
|
+
type: "text",
|
|
21066
|
+
text: `# PAPI Connection Error
|
|
21067
|
+
|
|
21068
|
+
${errorMessage}
|
|
21069
|
+
|
|
21070
|
+
## Quick Fix
|
|
21071
|
+
|
|
21072
|
+
If you haven't set up PAPI yet:
|
|
21073
|
+
1. Go to https://getpapi.ai/login and sign up
|
|
21074
|
+
2. Complete the onboarding wizard \u2014 it generates your config
|
|
21075
|
+
3. Copy the config to your project and restart your AI tool
|
|
21076
|
+
|
|
21077
|
+
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.`
|
|
21078
|
+
}]
|
|
21079
|
+
}));
|
|
21080
|
+
}
|
|
20828
21081
|
var transport = new StdioServerTransport();
|
|
20829
21082
|
await server.connect(transport);
|
package/package.json
CHANGED