@papi-ai/server 0.7.26 → 0.7.27

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 (2) hide show
  1. package/dist/index.js +329 -117
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -15950,13 +15950,23 @@ ${result.userMessage}
15950
15950
  }
15951
15951
 
15952
15952
  // src/services/strategy.ts
15953
- init_dist2();
15954
15953
  import { randomUUID as randomUUID8, createHash as createHash2 } from "crypto";
15955
15954
  import { execFileSync as execFileSync2 } from "child_process";
15956
15955
  import { existsSync, readdirSync, statSync } from "fs";
15957
15956
  import { join as join2 } from "path";
15958
15957
  import { homedir } from "os";
15959
15958
 
15959
+ // src/lib/hosted-mode.ts
15960
+ function isHostedTransport() {
15961
+ return Boolean(process.env.PORT || process.env.PAPI_HTTP_PORT);
15962
+ }
15963
+ function hasLocalWorkspace() {
15964
+ return !isHostedTransport();
15965
+ }
15966
+
15967
+ // src/services/strategy.ts
15968
+ init_dist2();
15969
+
15960
15970
  // src/lib/value-report.ts
15961
15971
  var MIN_SNAPSHOTS = 5;
15962
15972
  var MAX_SNAPSHOTS = 10;
@@ -16433,7 +16443,7 @@ ${lines.join("\n")}`;
16433
16443
  let unregisteredDocsText;
16434
16444
  try {
16435
16445
  const docsDir = join2(projectRoot, "docs");
16436
- if (existsSync(docsDir)) {
16446
+ if (hasLocalWorkspace() && existsSync(docsDir)) {
16437
16447
  const registeredPaths = new Set(
16438
16448
  (registeredDocs ?? []).map((d) => d.path).filter(Boolean)
16439
16449
  );
@@ -18786,6 +18796,56 @@ Run \`orient\` at the start of every session to get cycle state, in-progress tas
18786
18796
  - After build_execute completes: run \`review_submit\` with the verdict before presenting for human review
18787
18797
  `;
18788
18798
 
18799
+ // src/lib/files-to-write.ts
18800
+ function sanitiseProjectPath(path7) {
18801
+ if (!path7 || typeof path7 !== "string") {
18802
+ throw new Error("files_to_write: path must be a non-empty string");
18803
+ }
18804
+ if (path7.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path7)) {
18805
+ throw new Error(`files_to_write: absolute path rejected: ${path7}`);
18806
+ }
18807
+ const segments = path7.split(/[\\/]/);
18808
+ if (segments.some((s) => s === "..")) {
18809
+ throw new Error(`files_to_write: path traversal rejected: ${path7}`);
18810
+ }
18811
+ return segments.filter((s) => s.length > 0).join("/");
18812
+ }
18813
+ var FileWriteCollector = class {
18814
+ entries = [];
18815
+ add(entry) {
18816
+ this.entries.push({ ...entry, path: sanitiseProjectPath(entry.path) });
18817
+ }
18818
+ isEmpty() {
18819
+ return this.entries.length === 0;
18820
+ }
18821
+ list() {
18822
+ return this.entries;
18823
+ }
18824
+ };
18825
+ function formatFilesToWriteSection(collector) {
18826
+ if (collector.isEmpty()) return "";
18827
+ const json = JSON.stringify(collector.list(), null, 2);
18828
+ return [
18829
+ "",
18830
+ "---",
18831
+ "",
18832
+ "**SCAFFOLDING FILES \u2014 WRITE THESE LOCALLY**",
18833
+ "",
18834
+ "This MCP connection is remote \u2014 the server cannot write files to your project directory.",
18835
+ "Use your Write tool to materialise each entry below into the user's project root (your current working directory).",
18836
+ "",
18837
+ "```json files_to_write",
18838
+ json,
18839
+ "```",
18840
+ "",
18841
+ "Apply each entry in order. Semantics:",
18842
+ '- `mode: "create"` \u2014 create the file with `content`. If `skip_if_exists: true`, skip when the file already exists.',
18843
+ '- `mode: "overwrite"` \u2014 replace the file fully with `content`.',
18844
+ '- `mode: "append"` \u2014 append `content` to the existing file (create it first if missing).',
18845
+ ""
18846
+ ].join("\n");
18847
+ }
18848
+
18789
18849
  // src/services/setup.ts
18790
18850
  var FILE_TEMPLATES = {
18791
18851
  "PLANNING_LOG.md": PLANNING_LOG_TEMPLATE,
@@ -18809,7 +18869,7 @@ function substitute(template, vars) {
18809
18869
  }
18810
18870
  return result;
18811
18871
  }
18812
- async function scaffoldPapiDir(adapter2, config2, input) {
18872
+ async function scaffoldPapiDir(adapter2, config2, input, collector) {
18813
18873
  const isPg = config2.adapterType === "pg" || config2.adapterType === "proxy";
18814
18874
  const vars = {
18815
18875
  project_name: input.projectName,
@@ -18839,31 +18899,37 @@ async function scaffoldPapiDir(adapter2, config2, input) {
18839
18899
  } catch {
18840
18900
  }
18841
18901
  }
18842
- if (config2.adapterType === "proxy") {
18843
- return true;
18902
+ const useCollector = config2.adapterType === "proxy";
18903
+ const docsRel = "docs";
18904
+ const commandsRel = ".claude/commands";
18905
+ const commandsDir = useCollector ? commandsRel : join5(config2.projectRoot, ".claude", "commands");
18906
+ const docsDir = useCollector ? docsRel : join5(config2.projectRoot, "docs");
18907
+ if (!useCollector) {
18908
+ await mkdir(commandsDir, { recursive: true });
18909
+ await mkdir(docsDir, { recursive: true });
18844
18910
  }
18845
- const commandsDir = join5(config2.projectRoot, ".claude", "commands");
18846
- const docsDir = join5(config2.projectRoot, "docs");
18847
- await mkdir(commandsDir, { recursive: true });
18848
- await mkdir(docsDir, { recursive: true });
18849
- const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
18911
+ const claudeMdPath = useCollector ? "CLAUDE.md" : join5(config2.projectRoot, "CLAUDE.md");
18850
18912
  let claudeMdExists = false;
18851
- try {
18852
- await access2(claudeMdPath);
18853
- claudeMdExists = true;
18854
- } catch {
18913
+ if (!useCollector) {
18914
+ try {
18915
+ await access2(claudeMdPath);
18916
+ claudeMdExists = true;
18917
+ } catch {
18918
+ }
18855
18919
  }
18856
- const docsIndexPath = join5(docsDir, "INDEX.md");
18920
+ const docsIndexPath = useCollector ? `${docsRel}/INDEX.md` : join5(docsDir, "INDEX.md");
18857
18921
  let docsIndexExists = false;
18858
- try {
18859
- await access2(docsIndexPath);
18860
- docsIndexExists = true;
18861
- } catch {
18922
+ if (!useCollector) {
18923
+ try {
18924
+ await access2(docsIndexPath);
18925
+ docsIndexExists = true;
18926
+ } catch {
18927
+ }
18862
18928
  }
18863
18929
  const scaffoldFiles = {
18864
- [join5(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
18865
- [join5(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
18866
- [join5(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
18930
+ [useCollector ? `${commandsRel}/papi-audit.md` : join5(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
18931
+ [useCollector ? `${commandsRel}/test.md` : join5(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
18932
+ [useCollector ? `${docsRel}/README.md` : join5(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
18867
18933
  };
18868
18934
  if (!docsIndexExists) {
18869
18935
  scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
@@ -18880,29 +18946,42 @@ async function scaffoldPapiDir(adapter2, config2, input) {
18880
18946
  } catch {
18881
18947
  }
18882
18948
  }
18883
- for (const [dest, content] of Object.entries(planBundleInstall(config2.projectRoot, input.projectName, { skipExisting: true }))) {
18884
- await mkdir(dirname2(dest), { recursive: true });
18949
+ const bundleRoot = useCollector ? "" : config2.projectRoot;
18950
+ for (const [dest, content] of Object.entries(planBundleInstall(bundleRoot, input.projectName, { skipExisting: true }))) {
18951
+ if (!useCollector) {
18952
+ await mkdir(dirname2(dest), { recursive: true });
18953
+ }
18885
18954
  scaffoldFiles[dest] = content;
18886
18955
  }
18887
- const cursorDir = join5(config2.projectRoot, ".cursor");
18888
- let cursorDetected = false;
18889
- try {
18890
- await access2(cursorDir);
18891
- cursorDetected = true;
18892
- } catch {
18893
- }
18894
- if (cursorDetected) {
18895
- const cursorRulesDir = join5(cursorDir, "rules");
18896
- const cursorRulesPath = join5(cursorRulesDir, "papi.mdc");
18897
- await mkdir(cursorRulesDir, { recursive: true });
18956
+ if (useCollector) {
18957
+ scaffoldFiles[".cursor/rules/papi.mdc"] = substitute(CURSOR_RULES_TEMPLATE, vars);
18958
+ } else {
18959
+ const cursorDir = join5(config2.projectRoot, ".cursor");
18960
+ let cursorDetected = false;
18898
18961
  try {
18899
- await access2(cursorRulesPath);
18962
+ await access2(cursorDir);
18963
+ cursorDetected = true;
18900
18964
  } catch {
18901
- scaffoldFiles[cursorRulesPath] = substitute(CURSOR_RULES_TEMPLATE, vars);
18965
+ }
18966
+ if (cursorDetected) {
18967
+ const cursorRulesDir = join5(cursorDir, "rules");
18968
+ const cursorRulesPath = join5(cursorRulesDir, "papi.mdc");
18969
+ await mkdir(cursorRulesDir, { recursive: true });
18970
+ try {
18971
+ await access2(cursorRulesPath);
18972
+ } catch {
18973
+ scaffoldFiles[cursorRulesPath] = substitute(CURSOR_RULES_TEMPLATE, vars);
18974
+ }
18902
18975
  }
18903
18976
  }
18904
- for (const [filepath, content] of Object.entries(scaffoldFiles)) {
18905
- await writeFile2(filepath, content, "utf-8");
18977
+ if (useCollector) {
18978
+ for (const [relPath, content] of Object.entries(scaffoldFiles)) {
18979
+ collector.add({ path: relPath, content, mode: "create", skip_if_exists: true });
18980
+ }
18981
+ } else {
18982
+ for (const [filepath, content] of Object.entries(scaffoldFiles)) {
18983
+ await writeFile2(filepath, content, "utf-8");
18984
+ }
18906
18985
  }
18907
18986
  if (!isPg) {
18908
18987
  await adapter2.writePhases([{
@@ -18914,7 +18993,16 @@ async function scaffoldPapiDir(adapter2, config2, input) {
18914
18993
  order: 0
18915
18994
  }]);
18916
18995
  }
18917
- await ensurePapiPermission(config2.projectRoot);
18996
+ if (useCollector) {
18997
+ collector.add({
18998
+ path: ".claude/settings.json",
18999
+ content: JSON.stringify({ permissions: { allow: [PAPI_PERMISSION] } }, null, 2) + "\n",
19000
+ mode: "create",
19001
+ skip_if_exists: true
19002
+ });
19003
+ } else {
19004
+ await ensurePapiPermission(config2.projectRoot);
19005
+ }
18918
19006
  return true;
18919
19007
  }
18920
19008
  var PAPI_PERMISSION = "mcp__papi__*";
@@ -18943,7 +19031,7 @@ async function ensurePapiPermission(projectRoot) {
18943
19031
  } catch {
18944
19032
  }
18945
19033
  }
18946
- async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText, conventionsText) {
19034
+ async function applySetupOutputs(adapter2, config2, input, collector, briefText, adSeedText, conventionsText) {
18947
19035
  const warnings = [];
18948
19036
  await adapter2.updateProductBrief(briefText);
18949
19037
  const briefPhases = parsePhases(briefText);
@@ -19006,12 +19094,20 @@ async function applySetupOutputs(adapter2, config2, input, briefText, adSeedText
19006
19094
  }
19007
19095
  }
19008
19096
  }
19009
- if (conventionsText?.trim() && config2.adapterType !== "proxy") {
19010
- try {
19011
- const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
19012
- const existing = await readFile4(claudeMdPath, "utf-8");
19013
- await writeFile2(claudeMdPath, existing + "\n" + conventionsText.trim() + "\n", "utf-8");
19014
- } catch {
19097
+ if (conventionsText?.trim()) {
19098
+ if (config2.adapterType === "proxy") {
19099
+ collector.add({
19100
+ path: "CLAUDE.md",
19101
+ content: "\n" + conventionsText.trim() + "\n",
19102
+ mode: "append"
19103
+ });
19104
+ } else {
19105
+ try {
19106
+ const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
19107
+ const existing = await readFile4(claudeMdPath, "utf-8");
19108
+ await writeFile2(claudeMdPath, existing + "\n" + conventionsText.trim() + "\n", "utf-8");
19109
+ } catch {
19110
+ }
19015
19111
  }
19016
19112
  }
19017
19113
  return { seededAds, warnings };
@@ -19186,7 +19282,8 @@ function formatCodebaseSummary(scan, sourceContents) {
19186
19282
  return parts.join("\n");
19187
19283
  }
19188
19284
  async function prepareSetup(adapter2, config2, input) {
19189
- const createdProject = await scaffoldPapiDir(adapter2, config2, input);
19285
+ const prepareCollector = new FileWriteCollector();
19286
+ const createdProject = await scaffoldPapiDir(adapter2, config2, input, prepareCollector);
19190
19287
  let existingBrief;
19191
19288
  try {
19192
19289
  existingBrief = await adapter2.readProductBrief();
@@ -19291,11 +19388,13 @@ async function prepareSetup(adapter2, config2, input) {
19291
19388
  autoDetected: autoDetected && detectedCodebaseType !== "new_project",
19292
19389
  briefAlreadyExists: effectiveBriefAlreadyExists,
19293
19390
  briefWillRegenerate,
19294
- briefRegenReason
19391
+ briefRegenReason,
19392
+ filesToWrite: prepareCollector.isEmpty() ? void 0 : prepareCollector
19295
19393
  };
19296
19394
  }
19297
19395
  async function applySetup(adapter2, config2, input, briefText, adSeedText, conventionsText, initialTasksText) {
19298
- const createdProject = await scaffoldPapiDir(adapter2, config2, input);
19396
+ const collector = new FileWriteCollector();
19397
+ const createdProject = await scaffoldPapiDir(adapter2, config2, input, collector);
19299
19398
  const TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
19300
19399
  let effectiveBriefText = briefText;
19301
19400
  let briefRegenerated = false;
@@ -19320,7 +19419,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
19320
19419
  } catch {
19321
19420
  }
19322
19421
  }
19323
- const { seededAds, warnings } = await applySetupOutputs(adapter2, config2, input, effectiveBriefText, adSeedText, conventionsText);
19422
+ const { seededAds, warnings } = await applySetupOutputs(adapter2, config2, input, collector, effectiveBriefText, adSeedText, conventionsText);
19324
19423
  let createdTasks = 0;
19325
19424
  let tasksSkipped = 0;
19326
19425
  if (initialTasksText?.trim()) {
@@ -19367,29 +19466,33 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
19367
19466
  if (msg.startsWith("100% overlap")) throw err;
19368
19467
  }
19369
19468
  }
19370
- if (config2.adapterType !== "proxy") {
19371
- try {
19372
- const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
19373
- const existing = await readFile4(claudeMdPath, "utf-8");
19374
- if (!existing.includes("Dogfood Logging")) {
19375
- const dogfoodSection = [
19376
- "",
19377
- "## Dogfood Logging",
19378
- "",
19379
- "After each `release`, append a dogfood entry capturing observations from the cycle.",
19380
- "Call the adapter method with structured entries for each observation:",
19381
- "",
19382
- "- **friction** \u2014 workflow pain points, confusing flows, things that broke or slowed you down",
19383
- "- **methodology** \u2014 what worked or didn't in the plan/build/review cycle",
19384
- "- **signal** \u2014 indicators of product-market fit, user value, or growth potential",
19385
- "- **commercial** \u2014 cost, pricing, or business model observations",
19386
- "",
19387
- "This is autonomous plumbing \u2014 log observations after release without asking.",
19388
- ""
19389
- ].join("\n");
19390
- await writeFile2(claudeMdPath, existing + dogfoodSection, "utf-8");
19469
+ {
19470
+ const dogfoodSection = [
19471
+ "",
19472
+ "## Dogfood Logging",
19473
+ "",
19474
+ "After each `release`, append a dogfood entry capturing observations from the cycle.",
19475
+ "Call the adapter method with structured entries for each observation:",
19476
+ "",
19477
+ "- **friction** \u2014 workflow pain points, confusing flows, things that broke or slowed you down",
19478
+ "- **methodology** \u2014 what worked or didn't in the plan/build/review cycle",
19479
+ "- **signal** \u2014 indicators of product-market fit, user value, or growth potential",
19480
+ "- **commercial** \u2014 cost, pricing, or business model observations",
19481
+ "",
19482
+ "This is autonomous plumbing \u2014 log observations after release without asking.",
19483
+ ""
19484
+ ].join("\n");
19485
+ if (config2.adapterType === "proxy") {
19486
+ collector.add({ path: "CLAUDE.md", content: dogfoodSection, mode: "append" });
19487
+ } else {
19488
+ try {
19489
+ const claudeMdPath = join5(config2.projectRoot, "CLAUDE.md");
19490
+ const existing = await readFile4(claudeMdPath, "utf-8");
19491
+ if (!existing.includes("Dogfood Logging")) {
19492
+ await writeFile2(claudeMdPath, existing + dogfoodSection, "utf-8");
19493
+ }
19494
+ } catch {
19391
19495
  }
19392
- } catch {
19393
19496
  }
19394
19497
  }
19395
19498
  if (adapter2.writeDogfoodEntries) {
@@ -19412,12 +19515,26 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
19412
19515
  });
19413
19516
  } catch {
19414
19517
  }
19415
- const gitignoreNote = config2.adapterType === "proxy" ? void 0 : await ensureMcpJsonGitignored(config2.projectRoot);
19518
+ let gitignoreNote;
19519
+ if (config2.adapterType === "proxy") {
19520
+ collector.add({
19521
+ path: ".gitignore",
19522
+ content: "\n# PAPI: may contain credentials, do not commit\n.mcp.json\n",
19523
+ mode: "append"
19524
+ });
19525
+ gitignoreNote = "Added `.mcp.json` to `.gitignore` \u2014 may contain credentials, do not commit.";
19526
+ } else {
19527
+ gitignoreNote = await ensureMcpJsonGitignored(config2.projectRoot);
19528
+ }
19416
19529
  let cursorScaffolded = false;
19417
- try {
19418
- await access2(join5(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
19530
+ if (config2.adapterType === "proxy") {
19419
19531
  cursorScaffolded = true;
19420
- } catch {
19532
+ } else {
19533
+ try {
19534
+ await access2(join5(config2.projectRoot, ".cursor", "rules", "papi.mdc"));
19535
+ cursorScaffolded = true;
19536
+ } catch {
19537
+ }
19421
19538
  }
19422
19539
  return {
19423
19540
  createdProject,
@@ -19428,7 +19545,8 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
19428
19545
  briefRegenerated: briefRegenerated || void 0,
19429
19546
  cursorScaffolded,
19430
19547
  gitignoreNote,
19431
- warnings: warnings.length > 0 ? warnings : void 0
19548
+ warnings: warnings.length > 0 ? warnings : void 0,
19549
+ filesToWrite: collector.isEmpty() ? void 0 : collector
19432
19550
  };
19433
19551
  }
19434
19552
  async function ensureMcpJsonGitignored(projectRoot) {
@@ -19580,6 +19698,7 @@ ${[created, skipped].filter(Boolean).join(", ")}.`;
19580
19698
 
19581
19699
  \u26A0\uFE0F **Setup warnings (non-blocking):**
19582
19700
  ${result.warnings.map((w) => `- ${w}`).join("\n")}` : "";
19701
+ const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
19583
19702
  return textResponse(
19584
19703
  `${prefix}Product Brief generated and saved.${briefRegenNote}${adNote}${taskNote}${constraintsHint}${editorNote}${gitignoreNote}${warningsNote}
19585
19704
 
@@ -19589,7 +19708,7 @@ Tip: See \`docs/templates/example-project-brief.md\` for an example of a well-wr
19589
19708
 
19590
19709
  **Telemetry notice:** PAPI collects anonymous usage data (tool name, duration, project ID) to improve the product. No code, file contents, or personal data is collected. To opt out, add \`"PAPI_TELEMETRY": "off"\` to the \`env\` block in your \`.mcp.json\`.
19591
19710
 
19592
- Next step: run \`plan\` to start your first planning cycle.`
19711
+ Next step: run \`plan\` to start your first planning cycle.${filesToWriteSection}`
19593
19712
  );
19594
19713
  }
19595
19714
  async function handleSetup(adapter2, config2, args) {
@@ -19750,7 +19869,9 @@ ${result.initialTasksPrompt.user}
19750
19869
  result.codebaseSummary
19751
19870
  );
19752
19871
  }
19753
- return textResponse(sections.filter(Boolean).join("\n"));
19872
+ const responseText = sections.filter(Boolean).join("\n");
19873
+ const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
19874
+ return textResponse(responseText + filesToWriteSection);
19754
19875
  }
19755
19876
  } catch (err) {
19756
19877
  const message = err instanceof Error ? err.message : String(err);
@@ -19773,11 +19894,11 @@ ${result.initialTasksPrompt.user}
19773
19894
  init_dist2();
19774
19895
 
19775
19896
  // src/services/build.ts
19776
- init_git();
19777
- init_git();
19778
19897
  import { randomUUID as randomUUID9 } from "crypto";
19779
19898
  import { readdirSync as readdirSync4, existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync, unlinkSync, mkdirSync } from "fs";
19780
19899
  import { join as join6 } from "path";
19900
+ init_git();
19901
+ init_git();
19781
19902
  var buildStartTimes = /* @__PURE__ */ new Map();
19782
19903
  var taskBranchMap = /* @__PURE__ */ new Map();
19783
19904
  var taskStartShaMap = /* @__PURE__ */ new Map();
@@ -20191,20 +20312,37 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
20191
20312
  phaseChanges = await propagatePhaseStatus(adapter2);
20192
20313
  } catch {
20193
20314
  }
20315
+ const collector = new FileWriteCollector();
20194
20316
  try {
20195
- writeActiveTaskScope(config2.projectRoot, taskId, task.buildHandoff?.filesLikelyTouched);
20317
+ writeActiveTaskScope(
20318
+ config2.projectRoot,
20319
+ taskId,
20320
+ task.buildHandoff?.filesLikelyTouched,
20321
+ config2.adapterType,
20322
+ collector
20323
+ );
20196
20324
  } catch {
20197
20325
  }
20198
- return { task, branchLines, phaseChanges };
20326
+ return {
20327
+ task,
20328
+ branchLines,
20329
+ phaseChanges,
20330
+ filesToWrite: collector.isEmpty() ? void 0 : collector
20331
+ };
20199
20332
  }
20200
- function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched) {
20333
+ function writeActiveTaskScope(projectRoot, taskId, filesLikelyTouched, adapterType, collector) {
20334
+ const lines = [taskId, ...filesLikelyTouched ?? []];
20335
+ const content = lines.join("\n") + "\n";
20336
+ if (adapterType === "proxy") {
20337
+ collector.add({ path: ".papi/active-task-scope.txt", content, mode: "overwrite" });
20338
+ return;
20339
+ }
20201
20340
  const papiDir = join6(projectRoot, ".papi");
20202
20341
  if (!existsSync4(papiDir)) {
20203
20342
  mkdirSync(papiDir, { recursive: true });
20204
20343
  }
20205
20344
  const scopePath = join6(papiDir, "active-task-scope.txt");
20206
- const lines = [taskId, ...filesLikelyTouched ?? []];
20207
- writeFileSync(scopePath, lines.join("\n") + "\n", "utf-8");
20345
+ writeFileSync(scopePath, content, "utf-8");
20208
20346
  }
20209
20347
  function clearActiveTaskScope(projectRoot) {
20210
20348
  const scopePath = join6(projectRoot, ".papi", "active-task-scope.txt");
@@ -20527,7 +20665,7 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
20527
20665
  }
20528
20666
  let docWarning;
20529
20667
  try {
20530
- if (adapter2.searchDocs) {
20668
+ if (adapter2.searchDocs && hasLocalWorkspace()) {
20531
20669
  const docsDir = join6(config2.projectRoot, "docs");
20532
20670
  if (existsSync4(docsDir)) {
20533
20671
  const scanDir = (dir, depth = 0) => {
@@ -21037,7 +21175,8 @@ ${entries}`;
21037
21175
  }
21038
21176
  const moduleInstructions = getModuleInstructions(result.task.module);
21039
21177
  const moduleContext = await getModuleContext(adapter2, result.task);
21040
- return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote);
21178
+ const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
21179
+ return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote + filesToWriteSection);
21041
21180
  } catch (err) {
21042
21181
  if (isNoHandoffError(err)) {
21043
21182
  const lines = [
@@ -22186,6 +22325,7 @@ init_git();
22186
22325
  import { readFileSync as readFileSync3 } from "fs";
22187
22326
  import { join as join7 } from "path";
22188
22327
  function loadDocsIndex(projectRoot) {
22328
+ if (!hasLocalWorkspace()) return "";
22189
22329
  try {
22190
22330
  const indexPath = join7(projectRoot, "docs", "INDEX.md");
22191
22331
  const raw = readFileSync3(indexPath, "utf8");
@@ -22924,7 +23064,15 @@ function generateChangelogSection(version, commits) {
22924
23064
  ${commitList}
22925
23065
  `;
22926
23066
  }
22927
- async function writeChangelogSection(changelogPath, section, version) {
23067
+ async function writeChangelogSection(changelogPath, section, version, adapterType, collector) {
23068
+ if (adapterType === "proxy") {
23069
+ collector.add({
23070
+ path: "CHANGELOG.md",
23071
+ content: "\n" + section + "\n",
23072
+ mode: "append"
23073
+ });
23074
+ return;
23075
+ }
22928
23076
  let existing = "";
22929
23077
  try {
22930
23078
  existing = await readFile5(changelogPath, "utf-8");
@@ -23028,6 +23176,7 @@ async function resolveCycleToClose(adapter2, version) {
23028
23176
  return inferCycleFromVersion(version);
23029
23177
  }
23030
23178
  async function createRelease(config2, branch, version, adapter2, cycleNum, options) {
23179
+ const collector = new FileWriteCollector();
23031
23180
  if (!isGitAvailable()) {
23032
23181
  throw new Error("git is not available.");
23033
23182
  }
@@ -23165,11 +23314,15 @@ To override, pass force=true (emits a telemetry warning).`
23165
23314
  const changelogPath = join9(config2.projectRoot, "CHANGELOG.md");
23166
23315
  if (!latestTag) {
23167
23316
  const initialContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
23168
- await writeFile3(changelogPath, initialContent, "utf-8");
23317
+ if (config2.adapterType === "proxy") {
23318
+ collector.add({ path: "CHANGELOG.md", content: initialContent, mode: "create", skip_if_exists: true });
23319
+ } else {
23320
+ await writeFile3(changelogPath, initialContent, "utf-8");
23321
+ }
23169
23322
  } else {
23170
23323
  const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
23171
23324
  const section = generateChangelogSection(version, commits);
23172
- await writeChangelogSection(changelogPath, section, version);
23325
+ await writeChangelogSection(changelogPath, section, version, config2.adapterType, collector);
23173
23326
  }
23174
23327
  const commitResult = stageAllAndCommit(config2.projectRoot, `release: ${version}`);
23175
23328
  const commitNote = commitResult.committed ? `Committed CHANGELOG.md.` : `CHANGELOG.md: ${commitResult.message}`;
@@ -23196,7 +23349,8 @@ To override, pass force=true (emits a telemetry warning).`
23196
23349
  pushNotes,
23197
23350
  warnings: warnings.length > 0 ? warnings : void 0,
23198
23351
  ...groupedBranchMerges ? { groupedBranchMerges } : {},
23199
- cycleClosed: resolvedCycleNum
23352
+ cycleClosed: resolvedCycleNum,
23353
+ filesToWrite: collector.isEmpty() ? void 0 : collector
23200
23354
  };
23201
23355
  }
23202
23356
 
@@ -23323,7 +23477,8 @@ async function handleRelease(adapter2, config2, args) {
23323
23477
  }
23324
23478
  }
23325
23479
  lines.push("", `Next: cycle released! Run \`plan\` to start your next planning cycle.`);
23326
- return textResponse(lines.join("\n"));
23480
+ const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
23481
+ return textResponse(lines.join("\n") + filesToWriteSection);
23327
23482
  } catch (err) {
23328
23483
  const message = err instanceof Error ? err.message : String(err);
23329
23484
  const isKnownFriendly = /^(Release blocked|Working tree|gh CLI|Tag .* already exists|Branch .* not found|Cycle .* incomplete)/i.test(message);
@@ -24336,7 +24491,15 @@ async function tryAutoProvisionProject(opts) {
24336
24491
  }
24337
24492
  return { ok: true, projectId: body.project.id, projectName: body.project.name ?? opts.projectName };
24338
24493
  }
24339
- async function ensureGitignoreEntry(projectRoot, entry) {
24494
+ async function ensureGitignoreEntry(projectRoot, entry, adapterType, collector) {
24495
+ if (adapterType === "proxy") {
24496
+ collector.add({
24497
+ path: ".gitignore",
24498
+ content: "\n" + entry + "\n",
24499
+ mode: "append"
24500
+ });
24501
+ return;
24502
+ }
24340
24503
  const gitignorePath = path5.join(projectRoot, ".gitignore");
24341
24504
  let content = "";
24342
24505
  try {
@@ -24350,6 +24513,13 @@ async function ensureGitignoreEntry(projectRoot, entry) {
24350
24513
  const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
24351
24514
  await writeFile4(gitignorePath, content + separator + entry + "\n", "utf-8");
24352
24515
  }
24516
+ async function writeMcpConfig(absPath, relPath, content, isProjectScoped, adapterType, collector) {
24517
+ if (adapterType === "proxy" && isProjectScoped) {
24518
+ collector.add({ path: relPath, content, mode: "overwrite" });
24519
+ return;
24520
+ }
24521
+ await writeFile4(absPath, content, "utf-8");
24522
+ }
24353
24523
  async function handleInit(config2, args) {
24354
24524
  const projectRoot = config2.projectRoot;
24355
24525
  const force = args.force === true;
@@ -24363,6 +24533,19 @@ async function handleInit(config2, args) {
24363
24533
  }
24364
24534
  const agent = requestedAgent;
24365
24535
  const target = AGENT_TARGETS[agent];
24536
+ if (config2.adapterType === "proxy" && !target.isProjectScoped) {
24537
+ return errorResponse(
24538
+ [
24539
+ `Agent \`${agent}\` writes to a user-scoped config (${target.displayPath}) outside your project directory.`,
24540
+ "This MCP connection is remote \u2014 the server cannot write outside the project root.",
24541
+ "",
24542
+ "Workarounds:",
24543
+ "- Install the PAPI MCP server locally (`npx @papi-ai/server`) and run `init` over stdio.",
24544
+ "- Manually copy the config block from the getpapi.ai install snippet for this agent."
24545
+ ].join("\n")
24546
+ );
24547
+ }
24548
+ const collector = new FileWriteCollector();
24366
24549
  const configFileAbsPath = target.isProjectScoped ? path5.join(projectRoot, target.configPath) : target.configPath.startsWith("~/") ? path5.join(process.env.HOME ?? "", target.configPath.slice(2)) : target.configPath;
24367
24550
  const mcpJsonPath = configFileAbsPath;
24368
24551
  let existingConfig = null;
@@ -24423,12 +24606,12 @@ Path: ${mcpJsonPath}`
24423
24606
  const wroteToShared = !target.isProjectScoped && !!existingConfig;
24424
24607
  if (wroteToShared) {
24425
24608
  const sep = existingConfig.endsWith("\n") ? "\n" : "\n\n";
24426
- await writeFile4(mcpJsonPath, existingConfig + sep + fileBody, "utf-8");
24609
+ await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig + sep + fileBody, target.isProjectScoped, config2.adapterType, collector);
24427
24610
  } else {
24428
- await writeFile4(mcpJsonPath, fileBody, "utf-8");
24611
+ await writeMcpConfig(mcpJsonPath, target.configPath, fileBody, target.isProjectScoped, config2.adapterType, collector);
24429
24612
  }
24430
24613
  if (target.shouldGitignore) {
24431
- await ensureGitignoreEntry(projectRoot, target.configPath);
24614
+ await ensureGitignoreEntry(projectRoot, target.configPath, config2.adapterType, collector);
24432
24615
  }
24433
24616
  const writeNote = wroteToShared ? `Appended to ${target.displayPath} (preserved your existing entries \u2014 review the file to confirm structure).` : `Your new project ID and existing connection token are saved to ${target.displayPath}.`;
24434
24617
  return textResponse(
@@ -24444,7 +24627,7 @@ ${writeNote}
24444
24627
 
24445
24628
  1. **Restart your MCP client** to pick up the new config.
24446
24629
  2. **Run \`setup\`** \u2014 this scaffolds your project with a Product Brief and CLAUDE.md.
24447
- `
24630
+ ` + formatFilesToWriteSection(collector)
24448
24631
  );
24449
24632
  }
24450
24633
  if (provisioned.reason === "project_limit_reached") {
@@ -24490,12 +24673,12 @@ ${writeNote}
24490
24673
  const wroteToShared = !target.isProjectScoped && !!existingConfig;
24491
24674
  if (wroteToShared) {
24492
24675
  const sep = existingConfig.endsWith("\n") ? "\n" : "\n\n";
24493
- await writeFile4(mcpJsonPath, existingConfig + sep + fileBody, "utf-8");
24676
+ await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig + sep + fileBody, target.isProjectScoped, config2.adapterType, collector);
24494
24677
  } else {
24495
- await writeFile4(mcpJsonPath, fileBody, "utf-8");
24678
+ await writeMcpConfig(mcpJsonPath, target.configPath, fileBody, target.isProjectScoped, config2.adapterType, collector);
24496
24679
  }
24497
24680
  if (target.shouldGitignore) {
24498
- await ensureGitignoreEntry(projectRoot, target.configPath);
24681
+ await ensureGitignoreEntry(projectRoot, target.configPath, config2.adapterType, collector);
24499
24682
  }
24500
24683
  const writeNote = wroteToShared ? `Appended to ${target.displayPath} (preserved your existing entries \u2014 review the file to confirm structure).` : `Your existing API key and project ID have been saved to ${target.displayPath}.`;
24501
24684
  return textResponse(
@@ -24510,7 +24693,7 @@ ${writeNote}
24510
24693
 
24511
24694
  1. **Restart your MCP client** to pick up the new config.
24512
24695
  2. **Run \`setup\`** \u2014 this scaffolds your project with a Product Brief and CLAUDE.md.
24513
- `
24696
+ ` + formatFilesToWriteSection(collector)
24514
24697
  );
24515
24698
  }
24516
24699
  if (isDatabaseUser) {
@@ -24524,12 +24707,12 @@ ${writeNote}
24524
24707
  const wroteToShared = !target.isProjectScoped && !!existingConfig;
24525
24708
  if (wroteToShared) {
24526
24709
  const sep = existingConfig.endsWith("\n") ? "\n" : "\n\n";
24527
- await writeFile4(mcpJsonPath, existingConfig + sep + fileBody, "utf-8");
24710
+ await writeMcpConfig(mcpJsonPath, target.configPath, existingConfig + sep + fileBody, target.isProjectScoped, config2.adapterType, collector);
24528
24711
  } else {
24529
- await writeFile4(mcpJsonPath, fileBody, "utf-8");
24712
+ await writeMcpConfig(mcpJsonPath, target.configPath, fileBody, target.isProjectScoped, config2.adapterType, collector);
24530
24713
  }
24531
24714
  if (target.shouldGitignore) {
24532
- await ensureGitignoreEntry(projectRoot, target.configPath);
24715
+ await ensureGitignoreEntry(projectRoot, target.configPath, config2.adapterType, collector);
24533
24716
  }
24534
24717
  const output2 = [
24535
24718
  `# PAPI Initialised \u2014 ${projectName}`,
@@ -24544,7 +24727,7 @@ ${writeNote}
24544
24727
  ...process.env.DATABASE_URL ? ["1. **Restart your MCP client** to pick up the new config."] : [`1. **Set your DATABASE_URL** \u2014 replace \`<YOUR_DATABASE_URL>\` in \`${target.displayPath}\` with your Supabase **session pooler** connection string (port **5432**, not 6543 \u2014 the transaction pooler wedges on interrupted calls).`],
24545
24728
  "2. **Run `setup`** \u2014 this scaffolds your project with a Product Brief, Active Decisions, and CLAUDE.md."
24546
24729
  ].join("\n");
24547
- return textResponse(output2);
24730
+ return textResponse(output2 + formatFilesToWriteSection(collector));
24548
24731
  }
24549
24732
  const output = [
24550
24733
  `# PAPI \u2014 Account Required`,
@@ -25272,6 +25455,11 @@ async function handleDocScan(adapter2, config2, args) {
25272
25455
  if (!adapter2.searchDocs) {
25273
25456
  return errorResponse("Doc registry not available on this adapter.");
25274
25457
  }
25458
+ if (!hasLocalWorkspace()) {
25459
+ return textResponse(
25460
+ "doc_scan is unavailable on the hosted remote MCP transport \u2014 the server has no access to your local filesystem. Register docs explicitly via `doc_register` with the path/title/type you want indexed."
25461
+ );
25462
+ }
25275
25463
  const includePlans = args.include_plans ?? false;
25276
25464
  const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
25277
25465
  const registeredPaths = new Set(registered.map((d) => d.path));
@@ -26070,7 +26258,7 @@ async function handleOrient(adapter2, config2, args = {}) {
26070
26258
  }
26071
26259
  let unregisteredDocsNote2 = "";
26072
26260
  try {
26073
- if (adapter2.searchDocs) {
26261
+ if (adapter2.searchDocs && hasLocalWorkspace()) {
26074
26262
  const docsDir = join14(config2.projectRoot, "docs");
26075
26263
  const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
26076
26264
  if (docsFiles.length > 0) {
@@ -26126,10 +26314,12 @@ ${versionDrift}` : "";
26126
26314
  const deliveryShapeNote = deliveryShapeOutcome.status === "fulfilled" ? deliveryShapeOutcome.value : "";
26127
26315
  const { reconciliationNote, unrecordedNote, unregisteredDocsNote, staleSkillsNote } = deepHousekeepingOutcome.status === "fulfilled" ? deepHousekeepingOutcome.value : { reconciliationNote: "", unrecordedNote: "", unregisteredDocsNote: "", staleSkillsNote: "" };
26128
26316
  let enrichmentNote = "";
26317
+ const enrichmentCollector = new FileWriteCollector();
26129
26318
  try {
26130
- enrichmentNote = enrichClaudeMd(config2.projectRoot, healthResult.cycleNumber);
26319
+ enrichmentNote = enrichClaudeMd(config2.projectRoot, healthResult.cycleNumber, config2.adapterType, enrichmentCollector);
26131
26320
  } catch {
26132
26321
  }
26322
+ const enrichmentFilesSection = enrichmentCollector.isEmpty() ? "" : formatFilesToWriteSection(enrichmentCollector);
26133
26323
  let preBuildCheckNote = "";
26134
26324
  if (!cycleIsComplete) {
26135
26325
  const researchOrSpikeTasks = allTasks.filter((t) => {
@@ -26165,7 +26355,7 @@ ${section}`;
26165
26355
  tracker.mark("format-summary");
26166
26356
  const subAgents = await listAgents(config2.projectRoot);
26167
26357
  const deepHint = deepHousekeeping ? "" : "\n\n*Tip: pass `deep_housekeeping: true` to also check orphaned branches, unrecorded commits, unregistered docs, and stale skill forks.*";
26168
- return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents) + unblockNote + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint);
26358
+ return textResponse(projectBannerNote + formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment, subAgents) + unblockNote + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + staleSkillsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote + deliveryShapeNote + preBuildCheckNote + deepHint + enrichmentFilesSection);
26169
26359
  } catch (err) {
26170
26360
  const message = err instanceof Error ? err.message : String(err);
26171
26361
  const isKnownFriendly = /^(Orient failed|Project not found|No project|Setup required)/i.test(message);
@@ -26182,7 +26372,20 @@ ${section}`;
26182
26372
  }));
26183
26373
  }
26184
26374
  }
26185
- function enrichClaudeMd(projectRoot, cycleNumber) {
26375
+ function enrichClaudeMd(projectRoot, cycleNumber, adapterType, collector) {
26376
+ if (adapterType === "proxy") {
26377
+ const additions2 = [];
26378
+ if (cycleNumber >= 6) additions2.push(CLAUDE_MD_TIER_1);
26379
+ if (cycleNumber >= 21) additions2.push(CLAUDE_MD_TIER_2);
26380
+ if (additions2.length === 0) return "";
26381
+ collector.add({ path: "CLAUDE.md", content: additions2.join(""), mode: "append" });
26382
+ const tierNames2 = [];
26383
+ if (cycleNumber >= 6) tierNames2.push("Established (batch building, strategy reviews, AD lifecycle)");
26384
+ if (cycleNumber >= 21) tierNames2.push("Mature (idea pipeline, doc registry, advanced patterns)");
26385
+ return `
26386
+
26387
+ \u{1F4DD} **CLAUDE.md enriched** \u2014 added ${tierNames2.join(" + ")} guidance for cycle ${cycleNumber}+ projects.`;
26388
+ }
26186
26389
  const claudeMdPath = join14(projectRoot, "CLAUDE.md");
26187
26390
  if (!existsSync8(claudeMdPath)) return "";
26188
26391
  const content = readFileSync8(claudeMdPath, "utf-8");
@@ -26927,8 +27130,14 @@ async function runScopeBrief(adapter2, input) {
26927
27130
  const slug = input.taskId.replace(/[^a-z0-9-]/g, "-").toLowerCase();
26928
27131
  const relPath = `docs/scopes/${slug}.md`;
26929
27132
  const absPath = join15(input.projectRoot, relPath);
26930
- mkdirSync3(dirname3(absPath), { recursive: true });
26931
- writeFileSync4(absPath, addFrontmatter(docContent, task, input.cycleNumber), "utf-8");
27133
+ const docBody = addFrontmatter(docContent, task, input.cycleNumber);
27134
+ const collector = new FileWriteCollector();
27135
+ if (input.adapterType === "proxy") {
27136
+ collector.add({ path: relPath, content: docBody, mode: "overwrite" });
27137
+ } else {
27138
+ mkdirSync3(dirname3(absPath), { recursive: true });
27139
+ writeFileSync4(absPath, docBody, "utf-8");
27140
+ }
26932
27141
  const taskCount = countSubTasks(docContent);
26933
27142
  const summary = buildSummary(task, taskCount);
26934
27143
  if (!adapter2.registerDoc) {
@@ -26958,7 +27167,8 @@ ${decomposedNote}` : decomposedNote
26958
27167
  docPath: relPath,
26959
27168
  docId: entry.id,
26960
27169
  taskCount,
26961
- summary
27170
+ summary,
27171
+ filesToWrite: collector.isEmpty() ? void 0 : collector
26962
27172
  };
26963
27173
  }
26964
27174
  function buildTaskContext(task) {
@@ -27028,15 +27238,17 @@ async function handleScopeBrief(adapter2, config2, args) {
27028
27238
  taskId,
27029
27239
  apiKey,
27030
27240
  projectRoot: config2.projectRoot,
27031
- cycleNumber: health.totalCycles
27241
+ cycleNumber: health.totalCycles,
27242
+ adapterType: config2.adapterType
27032
27243
  });
27244
+ const filesToWriteSection = result.filesToWrite ? formatFilesToWriteSection(result.filesToWrite) : "";
27033
27245
  return textResponse(
27034
27246
  `**Scope document created:** \`${result.docPath}\`
27035
27247
  - **Sub-tasks defined:** ${result.taskCount}
27036
27248
  - **Doc registry ID:** ${result.docId}
27037
27249
  - **Source task:** ${taskId} marked as decomposed
27038
27250
 
27039
- **Next step:** Review \`${result.docPath}\` and submit each sub-task via \`idea\` with \`Reference: ${result.docPath}\` in notes. The planner will pick them up in the next cycle.`
27251
+ **Next step:** Review \`${result.docPath}\` and submit each sub-task via \`idea\` with \`Reference: ${result.docPath}\` in notes. The planner will pick them up in the next cycle.` + filesToWriteSection
27040
27252
  );
27041
27253
  } catch (err) {
27042
27254
  const message = err instanceof Error ? err.message : String(err);
@@ -27229,7 +27441,7 @@ async function handleDiscoveredIssueResolve(adapter2, args) {
27229
27441
  // src/tools/project.ts
27230
27442
  import path6 from "path";
27231
27443
  function workspacePapiDir(config2) {
27232
- if (process.env.PORT || process.env.PAPI_HTTP_PORT) return void 0;
27444
+ if (!hasLocalWorkspace()) return void 0;
27233
27445
  return config2.papiDir;
27234
27446
  }
27235
27447
  var NO_SUPPORT = "Project lifecycle tools require a hosted (proxy) or PostgreSQL (pg) adapter. The local md adapter does not support them.";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.26",
3
+ "version": "0.7.27",
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",