@papi-ai/server 0.7.105 → 0.7.106

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.
@@ -1908,11 +1908,13 @@ var init_proxy_adapter = __esm({
1908
1908
  endpoint;
1909
1909
  apiKey;
1910
1910
  projectId;
1911
+ papiDir;
1911
1912
  onAuthRejected;
1912
1913
  constructor(config) {
1913
1914
  this.endpoint = config.endpoint.replace(/\/$/, "");
1914
1915
  this.apiKey = config.apiKey;
1915
1916
  this.projectId = config.projectId ?? "";
1917
+ this.papiDir = config.papiDir ?? null;
1916
1918
  this.onAuthRejected = config.onAuthRejected;
1917
1919
  }
1918
1920
  /**
@@ -1988,6 +1990,7 @@ var init_proxy_adapter = __esm({
1988
1990
  endpoint: this.endpoint,
1989
1991
  apiKey: this.apiKey,
1990
1992
  projectId,
1993
+ papiDir: this.papiDir,
1991
1994
  onAuthRejected: this.onAuthRejected
1992
1995
  }));
1993
1996
  }
@@ -2620,7 +2623,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
2620
2623
  }
2621
2624
  // --- Project metadata (path-identity guardrail) ---
2622
2625
  async getProjectInfo() {
2623
- const raw = await this.invoke("getProjectInfo", []);
2626
+ const raw = await this.invoke("getProjectInfo", [this.papiDir]);
2624
2627
  if (!raw) return null;
2625
2628
  return {
2626
2629
  name: raw.name,
@@ -2707,7 +2710,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
2707
2710
  };
2708
2711
  }
2709
2712
  async listUserProjects() {
2710
- const body = await this.postRoute("project-list", {});
2713
+ const body = await this.postRoute("project-list", {
2714
+ ...this.papiDir ? { papiDir: this.papiDir } : {}
2715
+ });
2711
2716
  return body.projects ?? [];
2712
2717
  }
2713
2718
  async createUserProject(input) {
@@ -2966,6 +2971,7 @@ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the
2966
2971
  );
2967
2972
  }
2968
2973
  const pgAdapter = new PgAdapter(config);
2974
+ let projectCreated = false;
2969
2975
  try {
2970
2976
  const existing = await pgAdapter.getProject(projectId);
2971
2977
  if (!existing) {
@@ -2997,12 +3003,12 @@ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the
2997
3003
  id: projectId,
2998
3004
  slug,
2999
3005
  name: slug,
3000
- papi_dir: papiDir,
3001
3006
  user_id: userId,
3002
3007
  root_commit_hash: rootHash ?? void 0,
3003
3008
  repo_url: originUrl ?? void 0,
3004
3009
  resolution_method: rootHash ? "root_hash" : "manual"
3005
3010
  });
3011
+ projectCreated = true;
3006
3012
  }
3007
3013
  } else {
3008
3014
  if (!existing.root_commit_hash && rootHash || !existing.repo_url && originUrl) {
@@ -3023,25 +3029,6 @@ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the
3023
3029
  );
3024
3030
  }
3025
3031
  }
3026
- if (!_pathIdentityChecked) {
3027
- _pathIdentityChecked = true;
3028
- const cwd = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? process.cwd();
3029
- const projectName = existing.name ?? existing.slug ?? projectId;
3030
- const result = assertWorkspaceMatch({
3031
- storedPapiDir: existing.papi_dir,
3032
- projectName,
3033
- workspacePath: cwd
3034
- });
3035
- if (result && (result.action === "backfill" || result.action === "migrate")) {
3036
- try {
3037
- await pgAdapter.updateProject(projectId, { papi_dir: result.newPapiDir });
3038
- } catch (err) {
3039
- console.error(
3040
- `[papi] Failed to persist papi_dir update: ${err instanceof Error ? err.message : String(err)}`
3041
- );
3042
- }
3043
- }
3044
- }
3045
3032
  if (existing.user_id) {
3046
3033
  const configuredUserId = process.env["PAPI_USER_ID"] ?? detectUserId();
3047
3034
  if (configuredUserId && existing.user_id !== configuredUserId) {
@@ -3060,11 +3047,29 @@ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the
3060
3047
  } catch {
3061
3048
  }
3062
3049
  }
3063
- const adapter = new PgPapiAdapter({ ...config, userId: resolveUserId }, projectId);
3050
+ const adapter = new PgPapiAdapter({ ...config, userId: resolveUserId, papiDir }, projectId);
3064
3051
  try {
3065
3052
  await adapter.initRls();
3066
3053
  } catch {
3067
3054
  }
3055
+ if (projectCreated && papiDir && adapter.setProjectPapiDir) {
3056
+ await adapter.setProjectPapiDir(papiDir);
3057
+ }
3058
+ if (!_pathIdentityChecked && adapter.getProjectInfo && adapter.setProjectPapiDir) {
3059
+ _pathIdentityChecked = true;
3060
+ const info = await adapter.getProjectInfo();
3061
+ if (info) {
3062
+ const cwd = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? process.cwd();
3063
+ const result = assertWorkspaceMatch({
3064
+ storedPapiDir: info.papi_dir,
3065
+ projectName: info.name || info.slug || projectId,
3066
+ workspacePath: cwd
3067
+ });
3068
+ if (result && (result.action === "backfill" || result.action === "migrate")) {
3069
+ await adapter.setProjectPapiDir(result.newPapiDir);
3070
+ }
3071
+ }
3072
+ }
3068
3073
  const connected = await adapter.probeConnection();
3069
3074
  if (connected) {
3070
3075
  _connectionStatus = "connected";
@@ -3103,7 +3108,8 @@ Already have an account? Make sure PAPI_DATA_API_KEY is set in your .mcp.json en
3103
3108
  const adapter = new ProxyPapiAdapter2({
3104
3109
  endpoint: dataEndpoint,
3105
3110
  apiKey: dataApiKey,
3106
- projectId: projectId || void 0
3111
+ projectId: projectId || void 0,
3112
+ papiDir
3107
3113
  });
3108
3114
  const connected = await adapter.probeConnection();
3109
3115
  if (!connected) {
package/dist/index.js CHANGED
@@ -2744,11 +2744,13 @@ var init_proxy_adapter = __esm({
2744
2744
  endpoint;
2745
2745
  apiKey;
2746
2746
  projectId;
2747
+ papiDir;
2747
2748
  onAuthRejected;
2748
2749
  constructor(config2) {
2749
2750
  this.endpoint = config2.endpoint.replace(/\/$/, "");
2750
2751
  this.apiKey = config2.apiKey;
2751
2752
  this.projectId = config2.projectId ?? "";
2753
+ this.papiDir = config2.papiDir ?? null;
2752
2754
  this.onAuthRejected = config2.onAuthRejected;
2753
2755
  }
2754
2756
  /**
@@ -2824,6 +2826,7 @@ var init_proxy_adapter = __esm({
2824
2826
  endpoint: this.endpoint,
2825
2827
  apiKey: this.apiKey,
2826
2828
  projectId,
2829
+ papiDir: this.papiDir,
2827
2830
  onAuthRejected: this.onAuthRejected
2828
2831
  }));
2829
2832
  }
@@ -3456,7 +3459,7 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
3456
3459
  }
3457
3460
  // --- Project metadata (path-identity guardrail) ---
3458
3461
  async getProjectInfo() {
3459
- const raw = await this.invoke("getProjectInfo", []);
3462
+ const raw = await this.invoke("getProjectInfo", [this.papiDir]);
3460
3463
  if (!raw) return null;
3461
3464
  return {
3462
3465
  name: raw.name,
@@ -3543,7 +3546,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
3543
3546
  };
3544
3547
  }
3545
3548
  async listUserProjects() {
3546
- const body = await this.postRoute("project-list", {});
3549
+ const body = await this.postRoute("project-list", {
3550
+ ...this.papiDir ? { papiDir: this.papiDir } : {}
3551
+ });
3547
3552
  return body.projects ?? [];
3548
3553
  }
3549
3554
  async createUserProject(input) {
@@ -7290,6 +7295,7 @@ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the
7290
7295
  );
7291
7296
  }
7292
7297
  const pgAdapter = new PgAdapter(config2);
7298
+ let projectCreated = false;
7293
7299
  try {
7294
7300
  const existing = await pgAdapter.getProject(projectId);
7295
7301
  if (!existing) {
@@ -7321,12 +7327,12 @@ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the
7321
7327
  id: projectId,
7322
7328
  slug,
7323
7329
  name: slug,
7324
- papi_dir: papiDir,
7325
7330
  user_id: userId,
7326
7331
  root_commit_hash: rootHash ?? void 0,
7327
7332
  repo_url: originUrl ?? void 0,
7328
7333
  resolution_method: rootHash ? "root_hash" : "manual"
7329
7334
  });
7335
+ projectCreated = true;
7330
7336
  }
7331
7337
  } else {
7332
7338
  if (!existing.root_commit_hash && rootHash || !existing.repo_url && originUrl) {
@@ -7347,25 +7353,6 @@ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the
7347
7353
  );
7348
7354
  }
7349
7355
  }
7350
- if (!_pathIdentityChecked) {
7351
- _pathIdentityChecked = true;
7352
- const cwd = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? process.cwd();
7353
- const projectName = existing.name ?? existing.slug ?? projectId;
7354
- const result = assertWorkspaceMatch({
7355
- storedPapiDir: existing.papi_dir,
7356
- projectName,
7357
- workspacePath: cwd
7358
- });
7359
- if (result && (result.action === "backfill" || result.action === "migrate")) {
7360
- try {
7361
- await pgAdapter.updateProject(projectId, { papi_dir: result.newPapiDir });
7362
- } catch (err) {
7363
- console.error(
7364
- `[papi] Failed to persist papi_dir update: ${err instanceof Error ? err.message : String(err)}`
7365
- );
7366
- }
7367
- }
7368
- }
7369
7356
  if (existing.user_id) {
7370
7357
  const configuredUserId = process.env["PAPI_USER_ID"] ?? detectUserId();
7371
7358
  if (configuredUserId && existing.user_id !== configuredUserId) {
@@ -7384,11 +7371,29 @@ Fix the PAPI_PROJECT_ID in your .mcp.json (or unset it and let PAPI resolve the
7384
7371
  } catch {
7385
7372
  }
7386
7373
  }
7387
- const adapter2 = new PgPapiAdapter({ ...config2, userId: resolveUserId }, projectId);
7374
+ const adapter2 = new PgPapiAdapter({ ...config2, userId: resolveUserId, papiDir }, projectId);
7388
7375
  try {
7389
7376
  await adapter2.initRls();
7390
7377
  } catch {
7391
7378
  }
7379
+ if (projectCreated && papiDir && adapter2.setProjectPapiDir) {
7380
+ await adapter2.setProjectPapiDir(papiDir);
7381
+ }
7382
+ if (!_pathIdentityChecked && adapter2.getProjectInfo && adapter2.setProjectPapiDir) {
7383
+ _pathIdentityChecked = true;
7384
+ const info = await adapter2.getProjectInfo();
7385
+ if (info) {
7386
+ const cwd = options.projectRoot ?? process.env["PAPI_PROJECT_DIR"] ?? process.cwd();
7387
+ const result = assertWorkspaceMatch({
7388
+ storedPapiDir: info.papi_dir,
7389
+ projectName: info.name || info.slug || projectId,
7390
+ workspacePath: cwd
7391
+ });
7392
+ if (result && (result.action === "backfill" || result.action === "migrate")) {
7393
+ await adapter2.setProjectPapiDir(result.newPapiDir);
7394
+ }
7395
+ }
7396
+ }
7392
7397
  const connected = await adapter2.probeConnection();
7393
7398
  if (connected) {
7394
7399
  _connectionStatus = "connected";
@@ -7427,7 +7432,8 @@ Already have an account? Make sure PAPI_DATA_API_KEY is set in your .mcp.json en
7427
7432
  const adapter2 = new ProxyPapiAdapter2({
7428
7433
  endpoint: dataEndpoint,
7429
7434
  apiKey: dataApiKey,
7430
- projectId: projectId || void 0
7435
+ projectId: projectId || void 0,
7436
+ papiDir
7431
7437
  });
7432
7438
  const connected = await adapter2.probeConnection();
7433
7439
  if (!connected) {
@@ -7556,21 +7562,27 @@ import {
7556
7562
  // src/universal-frame.ts
7557
7563
  var UNIVERSAL_FRAME = `PAPI gives this project a structured plan \u2192 build \u2192 review cycle, persisted across sessions. Follow it:
7558
7564
 
7559
- 0. NEW HERE? If PAPI has never been set up for this project (no cycles yet, or \`orient\` says the board is empty), call \`setup\` FIRST \u2014 it generates the Product Brief and scaffolds the workflow. Then run \`plan\` to create the first cycle. This is the required first step for a brand-new project; everything below assumes setup has already run.
7565
+ 0a. MCHAT REALITY. Most hosts load MCP tools at CONVERSATION START \u2014 tools added or authenticated mid-chat are invisible in that chat. If the user says PAPI "is not working" or "failed to connect" but your tool list has no papi tools, do NOT retry the install or re-run OAuth: the link almost certainly succeeded already. Tell them plainly: "PAPI connected fine \u2014 tools only load in a NEW conversation. Start a fresh chat and run \`papi\` there." Never claim the connection failed when you simply cannot see the tools.
7566
+
7567
+ 0b. ONE CHAT, ONE PROJECT. Each conversation binds to ONE PAPI project at a time. If the user has several projects, orient names which one is connected \u2014 confirm it matches the repo the user is working in before writing anything. Wrong project? Run \`project_switch\` (or pass \`project="<slug>"\` per call), and say a fresh conversation is the cleanest switch.
7568
+
7569
+ 1. NEW HERE? If PAPI has never been set up for this project (no cycles yet, or \`orient\` says the board is empty), call \`setup\` FIRST \u2014 it generates the Product Brief and scaffolds the workflow. Then run \`plan\` to create the first cycle. This is the required first step for a brand-new project; everything below assumes setup has already run.
7560
7570
 
7561
- 1. ORIENT FIRST (once set up). At the start of every session, call \`orient\` (or \`papi\`) before anything else \u2014 it returns the current cycle, what's in flight, and the recommended next action. Re-run it after any context compression.
7571
+ 2. ORIENT FIRST (once set up). At the start of every session, call \`orient\` (or \`papi\`) before anything else \u2014 it returns the current cycle, what's in flight, and the recommended next action. Re-run it after any context compression.
7562
7572
 
7563
- 2. THE CYCLE, IN ORDER: \`plan\` (once per cycle) \u2192 \`build_list\` (pick a task) \u2192 \`build_execute <task>\` to start \u2192 implement the task from its BUILD HANDOFF \u2192 \`build_execute\` again to complete with a build report \u2192 \`review_submit\` \u2192 \`release\` when every cycle task is done.
7573
+ 3. THE CYCLE, IN ORDER: \`plan\` (once per cycle) \u2192 \`build_list\` (pick a task) \u2192 \`build_execute <task>\` to start \u2192 implement the task from its BUILD HANDOFF \u2192 \`build_execute\` again to complete with a build report \u2192 \`review_submit\` \u2192 \`release\` when every cycle task is done.
7564
7574
 
7565
- 3. STATUS DISCIPLINE. Check a task's status before acting. "In Review" = already built \u2014 never re-build it; submit a review instead. "In Progress" = a build started \u2014 check the existing branch before writing new code. "Backlog" = not started, but check for an existing feature branch first.
7575
+ 4. STATUS DISCIPLINE. Check a task's status before acting. "In Review" = already built \u2014 never re-build it; submit a review instead. "In Progress" = a build started \u2014 check the existing branch before writing new code. "Backlog" = not started, but check for an existing feature branch first.
7566
7576
 
7567
- 4. STAY IN SCOPE. Build exactly what the BUILD HANDOFF specifies. Mechanical steps (branch creation, commits, status updates) are automatic \u2014 only "what to build" needs the user's confirmation.
7577
+ 5. STAY IN SCOPE. Build exactly what the BUILD HANDOFF specifies. Mechanical steps (branch creation, commits, status updates) are automatic \u2014 only "what to build" needs the user's confirmation.
7568
7578
 
7569
- 5. VERIFY BEFORE DONE. Test the change and confirm it works before reporting a task complete. Report failures honestly, with the output.
7579
+ 6. VERIFY BEFORE DONE. Test the change and confirm it works before reporting a task complete. Report failures honestly, with the output.
7570
7580
 
7571
- 6. NAME THE PROJECT WHEN YOU KNOW IT. If you know which repo this session is working in, pass \`project="<slug>"\` on the call rather than relying on whatever the connection defaults to. An account with more than one project cannot be resolved from a connection with no project bound \u2014 PAPI will stop and ask which one rather than guess, and answering costs a round trip. If PAPI asks, put the list to the user, then re-call with their choice; run \`project_switch\` to make it stick.
7581
+ 7. NAME THE PROJECT WHEN YOU KNOW IT. If you know which repo this session is working in, pass \`project="<slug>"\` on the call rather than relying on whatever the connection defaults to. An account with more than one project cannot be resolved from a connection with no project bound \u2014 PAPI will stop and ask which one rather than guess, and answering costs a round trip. If PAPI asks, put the list to the user, then re-call with their choice; run \`project_switch\` to make it stick.
7572
7582
 
7573
- PAPI reads and writes all project state through these tools \u2014 they are the source of truth, not local files.`;
7583
+ PAPI reads and writes all project state through these tools \u2014 they are the source of truth, not local files.
7584
+
7585
+ DATA HANDLING: PAPI stores structured project metadata only (never your source code), runs no AI calls from its servers \u2014 https://getpapi.ai/trust states exactly who can read what.`;
7574
7586
 
7575
7587
  // src/lib/response.ts
7576
7588
  function textResponse(text, usage) {
@@ -12805,6 +12817,13 @@ async function resolveCallerLatestCycle(adapter2, callerUserId) {
12805
12817
  }
12806
12818
  return newest;
12807
12819
  }
12820
+ function unownedCycleNumbers(cycles) {
12821
+ const unowned = /* @__PURE__ */ new Set();
12822
+ for (const c of cycles) {
12823
+ if (c.userId == null) unowned.add(c.number);
12824
+ }
12825
+ return unowned;
12826
+ }
12808
12827
  async function validateAndPrepare(adapter2, force, callerUserId, adapterType) {
12809
12828
  let mode;
12810
12829
  let cycleNumber;
@@ -12829,23 +12848,37 @@ Run \`release\` first, or pass \`force: true\` to bypass this block.`
12829
12848
  );
12830
12849
  }
12831
12850
  if (!force) {
12851
+ const callerCycleNumbers = callerUserId ? (await adapter2.readCycles()).filter((c) => c.userId === callerUserId).map((c) => c.number) : null;
12832
12852
  const inReviewTasks = await adapter2.queryBoard({
12833
12853
  status: ["In Review"],
12834
12854
  compact: true
12835
12855
  });
12836
- const staleTasks = inReviewTasks.filter(
12837
- (t) => t.cycle !== void 0 && t.cycle <= cycleNumber - 2
12856
+ const staleMine = inReviewTasks.filter(
12857
+ (t) => t.cycle !== void 0 && t.cycle <= cycleNumber - 2 && (callerCycleNumbers === null || callerCycleNumbers.includes(t.cycle))
12838
12858
  );
12839
- if (staleTasks.length > 0) {
12840
- const taskList = staleTasks.map((t) => `- ${t.id} (Cycle ${t.cycle}): ${t.title}`).join("\n");
12859
+ if (staleMine.length > 0) {
12860
+ const taskList = staleMine.map((t) => `- ${t.id} (Cycle ${t.cycle}): ${t.title}`).join("\n");
12841
12861
  throw new Error(
12842
- `Stale reviews detected \u2014 ${staleTasks.length} task(s) have been In Review for 2+ cycles:
12862
+ `Stale reviews detected \u2014 ${staleMine.length} task(s) have been In Review for 2+ cycles:
12843
12863
 
12844
12864
  ${taskList}
12845
12865
 
12846
12866
  Run \`review_submit\` to clear them, or pass \`force: true\` to bypass this block.`
12847
12867
  );
12848
12868
  }
12869
+ if (callerCycleNumbers !== null) {
12870
+ const unowned = unownedCycleNumbers(await adapter2.readCycles());
12871
+ const staleTheirs = inReviewTasks.filter(
12872
+ (t) => t.cycle !== void 0 && t.cycle <= cycleNumber - 2 && !callerCycleNumbers.includes(t.cycle) && // A task in a cycle with no known owner is unowned legacy data —
12873
+ // do not attribute it to a teammate either.
12874
+ !unowned.has(t.cycle)
12875
+ );
12876
+ if (staleTheirs.length > 0) {
12877
+ strategyReviewWarning += `> \u2139\uFE0F ${staleTheirs.length} task(s) are In Review from other members' cycles (2+ cycles stale). They do not block your planning \u2014 but the review queue may need attention.
12878
+
12879
+ `;
12880
+ }
12881
+ }
12849
12882
  }
12850
12883
  const autoCompleted = await assertSingleActiveCycle(adapter2, { autoComplete: force === true, userId: callerUserId ?? void 0 });
12851
12884
  for (const note of autoCompleted) {
@@ -32105,10 +32138,18 @@ async function handleOrient(rawAdapter, config2, args = {}, clientName, serverVe
32105
32138
  const handshake = buildHandshakeResult(proxyVersion);
32106
32139
  return handshake.mismatch && handshake.warning ? handshake.warning : void 0;
32107
32140
  }),
32108
- // P1 stall warning: P1 High tasks in Backlog that haven't moved in 3+ cycles
32141
+ // P1 stall warning: P1 High tasks in Backlog that haven't moved in 3+ cycles.
32142
+ // task-3736: the read is scoped to the CALLER (unassigned-or-mine, the same
32143
+ // predicate task-3397 pinned). Unscoped, a teammate's Backlog P1s were aged
32144
+ // against the CALLER's cycle count — a member at cycle 372 reading a
32145
+ // teammate's cycle-5 task as "367+ cycles" stalled. Cycle identity is
32146
+ // (project_id, user_id, number) since task-3335; cross-member cycle-age
32147
+ // arithmetic is meaningless. The caller's own unclaimed pool P1s still
32148
+ // count — unclaimed work is everyone's to claim, and its createdCycle is
32149
+ // the cycle that planned it.
32109
32150
  tracked("p1-stall", async () => {
32110
32151
  const p1BacklogTasks = await adapter2.queryBoard({ status: ["Backlog"], priority: ["P1 High"] });
32111
- const stalledP1 = p1BacklogTasks.filter(
32152
+ const stalledP1 = scopeTasksToCaller(p1BacklogTasks, callerUserId).filter(
32112
32153
  (t) => t.createdCycle != null && currentCycle2 - t.createdCycle >= 3
32113
32154
  );
32114
32155
  if (stalledP1.length === 0) return void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papi-ai/server",
3
- "version": "0.7.105",
3
+ "version": "0.7.106",
4
4
  "description": "PAPI MCP server — AI-powered sprint planning, build execution, and strategy review for software projects",
5
5
  "license": "Elastic-2.0",
6
6
  "mcpName": "io.github.getpapi/papi",