@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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +319 -66
  3. 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://papi-web-three.vercel.app/landing) and copy your `.mcp.json` snippet
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://papi-web-three.vercel.app/landing)
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://papi-web-three.vercel.app";
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 handoffs2 = (data.cycleHandoffs ?? []).map((h) => {
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 = handoffs2.length;
11734
- const legacyEffortPoints = handoffs2.reduce((sum, h) => sum + (effortMapLegacy[h.handoff.effort] ?? 3), 0);
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: handoffs2,
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 cycleTaskCount = handoffs.length;
11834
- const cycleEffortPoints = handoffs.reduce((sum, h) => sum + (effortMap[h.handoff.effort] ?? 3), 0);
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.getProductBrief()
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]?.date ? new Date(previousStrategyReviews[0].date) : /* @__PURE__ */ new Date(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" ? "deleted" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
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 Promise.all(recs.map((rec) => adapter2.writeRecommendation(rec)));
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" ? "deleted" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
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: "strategy_capture",
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 === "Not Started" || mapping.horizon.status === "Not Started";
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 Not Started stages/horizons)");
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 Not Started. Consider deferring or reassigning.`);
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.readActiveDecisions();
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 projectId = randomUUID14();
19372
- const mcpConfig = {
19373
- mcpServers: {
19374
- papi: {
19375
- command: "npx",
19376
- args: ["-y", "@papi-ai/server"],
19377
- env: {
19378
- PAPI_PROJECT_DIR: projectRoot,
19379
- PAPI_ADAPTER: "pg",
19380
- DATABASE_URL: "<YOUR_DATABASE_URL>",
19381
- PAPI_PROJECT_ID: projectId
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
- await writeFile4(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
19387
- await ensureGitignoreEntry(projectRoot, ".mcp.json");
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 Initialised \u2014 ${projectName}`,
19563
+ `# PAPI \u2014 Account Required`,
19564
+ "",
19565
+ `PAPI needs an account to store your project data.`,
19390
19566
  "",
19391
- `**Project ID:** \`${projectId}\``,
19392
- `**Project directory:** \`${projectRoot}\`${usingCwdDefault ? " *(detected from current directory)*" : ""}`,
19393
- `**Config:** \`${mcpJsonPath}\``,
19567
+ "## Get Started in 3 Steps",
19394
19568
  "",
19395
- "## Next Steps",
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
- "1. **Set your DATABASE_URL** \u2014 replace `<YOUR_DATABASE_URL>` in `.mcp.json` with the connection string provided to you.",
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
- "> `.mcp.json` uses `npx @papi-ai/server` \u2014 no local paths to maintain. See `docs/install-guide.md` for multi-client configs."
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 === "Active") || horizons[0];
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 === "Active") || activeStages[0];
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 __dirname = dirname(__filename);
20583
- const skillsDir = join10(__dirname, "..", "skills");
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 \`${config2.projectId}\`. Run \`setup\` first to initialise your project.`
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 = await createAdapter({
20823
- adapterType: config.adapterType,
20824
- papiDir: config.papiDir,
20825
- papiEndpoint: config.papiEndpoint
20826
- });
20827
- var server = createServer(adapter, config);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",