@vailent/pulse-mcp 1.1.0 → 1.3.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 (2) hide show
  1. package/dist/server.js +131 -19
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -40498,14 +40498,32 @@ async function handlePodMembers(params) {
40498
40498
  return ok(data, `${(data || []).length} members in pod`, (data || []).length);
40499
40499
  }
40500
40500
  case "add": {
40501
- const displayName = params.displayName;
40502
- if (!displayName?.trim()) return err("displayName is required", "MISSING_PARAM");
40503
- const initials = params.initials || displayName.split(" ").map((w) => w[0]).join("").toUpperCase().slice(0, 2);
40501
+ const userId = params.userId;
40502
+ let displayName = params.displayName;
40503
+ let initials = params.initials;
40504
+ let role = params.role || "engineer";
40505
+ if (userId) {
40506
+ const { data: user } = await supabase.from("users").select("full_name, job_title").eq("id", userId).single();
40507
+ if (!user) return err("User not found", "NOT_FOUND");
40508
+ if (!displayName) displayName = user.full_name;
40509
+ if (!initials) {
40510
+ initials = displayName.split(" ").map((w) => w[0]).join("").toUpperCase().slice(0, 2);
40511
+ }
40512
+ if (!params.role) {
40513
+ const jobTitles = user.job_title || ["member"];
40514
+ role = jobTitles.includes("product_lead") ? "product_lead" : jobTitles.includes("designer") ? "designer" : "engineer";
40515
+ }
40516
+ }
40517
+ if (!displayName?.trim()) return err("displayName is required (or provide userId)", "MISSING_PARAM");
40518
+ if (!initials) {
40519
+ initials = displayName.split(" ").map((w) => w[0]).join("").toUpperCase().slice(0, 2);
40520
+ }
40504
40521
  const { data, error: error2 } = await supabase.from("pod_members").insert({
40505
40522
  pod_id: podId,
40523
+ user_id: userId || null,
40506
40524
  display_name: displayName.trim(),
40507
40525
  initials,
40508
- role: params.role || "engineer"
40526
+ role
40509
40527
  }).select().single();
40510
40528
  if (error2) return err(error2.message);
40511
40529
  return ok(data, `Added ${displayName} to pod`);
@@ -40533,6 +40551,74 @@ async function handlePodMembers(params) {
40533
40551
  }
40534
40552
  }
40535
40553
 
40554
+ // lib/current-user.ts
40555
+ import { execSync } from "child_process";
40556
+ var cached2;
40557
+ async function getCurrentUser() {
40558
+ if (cached2 !== void 0) return cached2;
40559
+ try {
40560
+ cached2 = await resolveUser();
40561
+ if (cached2) {
40562
+ console.error(`\u2713 Pulse: identified as ${cached2.fullName} (${cached2.email})`);
40563
+ }
40564
+ return cached2;
40565
+ } catch {
40566
+ cached2 = null;
40567
+ return null;
40568
+ }
40569
+ }
40570
+ async function resolveUser() {
40571
+ const supabase = getAdminClient();
40572
+ const envEmail = process.env.PULSE_USER_EMAIL;
40573
+ if (envEmail?.endsWith("@vailent.com")) {
40574
+ const user = await lookupByEmail(supabase, envEmail);
40575
+ if (user) return user;
40576
+ }
40577
+ const localEmail = getGitEmail("local");
40578
+ if (localEmail?.endsWith("@vailent.com")) {
40579
+ const user = await lookupByEmail(supabase, localEmail);
40580
+ if (user) return user;
40581
+ }
40582
+ const globalEmail = getGitEmail("global");
40583
+ if (globalEmail?.endsWith("@vailent.com")) {
40584
+ const user = await lookupByEmail(supabase, globalEmail);
40585
+ if (user) return user;
40586
+ }
40587
+ const anyEmail = localEmail || globalEmail;
40588
+ if (anyEmail) {
40589
+ console.error(
40590
+ `\u26A0 Pulse: Could not identify you.
40591
+ Your git email (${anyEmail}) is not a @vailent.com address.
40592
+ Fix: git config --global user.email yourname@vailent.com
40593
+ Then restart Claude Code.`
40594
+ );
40595
+ } else {
40596
+ console.error(
40597
+ `\u26A0 Pulse: Could not identify you.
40598
+ Git email not configured.
40599
+ Fix: git config --global user.email yourname@vailent.com
40600
+ Then restart Claude Code.`
40601
+ );
40602
+ }
40603
+ return null;
40604
+ }
40605
+ function getGitEmail(scope) {
40606
+ try {
40607
+ const flag = scope === "local" ? "--local" : "--global";
40608
+ return execSync(`git config ${flag} user.email`, {
40609
+ encoding: "utf-8",
40610
+ timeout: 3e3
40611
+ }).trim() || null;
40612
+ } catch {
40613
+ return null;
40614
+ }
40615
+ }
40616
+ async function lookupByEmail(supabase, email3) {
40617
+ const { data } = await supabase.from("users").select("id, email, full_name, job_title").eq("email", email3).limit(1).single();
40618
+ if (!data) return null;
40619
+ return { id: data.id, email: data.email, fullName: data.full_name, jobTitle: data.job_title || ["member"] };
40620
+ }
40621
+
40536
40622
  // tools/features.ts
40537
40623
  async function handleFeatures(params) {
40538
40624
  const supabase = getAdminClient();
@@ -40540,7 +40626,16 @@ async function handleFeatures(params) {
40540
40626
  switch (action) {
40541
40627
  case "list": {
40542
40628
  let query = supabase.from("features").select("*").order("week_start", { ascending: false });
40543
- if (params.podId) query = query.eq("pod_id", params.podId);
40629
+ if (params.podId) {
40630
+ query = query.eq("pod_id", params.podId);
40631
+ } else {
40632
+ const currentUser = await getCurrentUser();
40633
+ if (currentUser) {
40634
+ const { data: memberships } = await supabase.from("pod_members").select("pod_id").eq("user_id", currentUser.id);
40635
+ const myPodIds = (memberships || []).map((m) => m.pod_id);
40636
+ if (myPodIds.length > 0) query = query.in("pod_id", myPodIds);
40637
+ }
40638
+ }
40544
40639
  if (params.projectName) query = query.eq("project_name", params.projectName);
40545
40640
  if (params.weekStart) query = query.eq("week_start", params.weekStart);
40546
40641
  if (params.status) query = query.eq("status", params.status);
@@ -40597,7 +40692,10 @@ async function handleFeatures(params) {
40597
40692
  weekStart: "week_start",
40598
40693
  size: "size",
40599
40694
  projectName: "project_name",
40600
- sortOrder: "sort_order"
40695
+ sortOrder: "sort_order",
40696
+ prdText: "prd_text",
40697
+ prdFileUrl: "prd_file_url",
40698
+ prdFileName: "prd_file_name"
40601
40699
  };
40602
40700
  const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
40603
40701
  for (const [key, col] of Object.entries(allowedFields)) {
@@ -42067,9 +42165,9 @@ var multipartFormRequestOptions = async (opts, fetch2, stripFilenames = true) =>
42067
42165
  var supportsFormDataMap = /* @__PURE__ */ new WeakMap();
42068
42166
  function supportsFormData(fetchObject) {
42069
42167
  const fetch2 = typeof fetchObject === "function" ? fetchObject : fetchObject.fetch;
42070
- const cached2 = supportsFormDataMap.get(fetch2);
42071
- if (cached2)
42072
- return cached2;
42168
+ const cached3 = supportsFormDataMap.get(fetch2);
42169
+ if (cached3)
42170
+ return cached3;
42073
42171
  const promise2 = (async () => {
42074
42172
  try {
42075
42173
  const FetchResponse = "Response" in fetch2 ? fetch2.Response : (await fetch2("data:,")).constructor;
@@ -47392,7 +47490,10 @@ function formatWeekStart(date4) {
47392
47490
  }
47393
47491
  async function handleBriefing(params) {
47394
47492
  const supabase = getAdminClient();
47395
- const jobTitle = params.jobTitle || "engineer";
47493
+ const currentUser = await getCurrentUser();
47494
+ const paramTitle = params.jobTitle;
47495
+ const jobTitles = paramTitle ? [paramTitle] : currentUser?.jobTitle || ["engineer"];
47496
+ const jobTitle = jobTitles.find((t) => t === "leadership") || jobTitles.find((t) => t === "product_lead") || jobTitles.find((t) => t === "designer") || jobTitles.find((t) => t === "engineer") || jobTitles.find((t) => t === "sales") || jobTitles[0] || "engineer";
47396
47497
  const podIds = params.podIds;
47397
47498
  const now = /* @__PURE__ */ new Date();
47398
47499
  const thisWeekStart = getWeekStart(now);
@@ -47461,7 +47562,7 @@ async function handleTeam(params) {
47461
47562
  id: u.id,
47462
47563
  email: u.email,
47463
47564
  fullName: u.full_name,
47464
- jobTitle: u.job_title || "member",
47565
+ jobTitle: u.job_title || ["member"],
47465
47566
  role: u.role,
47466
47567
  isActive: u.is_active,
47467
47568
  pods: userPods
@@ -47471,11 +47572,16 @@ async function handleTeam(params) {
47471
47572
  }
47472
47573
  case "update_role": {
47473
47574
  const userId = params.userId;
47474
- const jobTitle = params.jobTitle;
47475
- if (!userId || !jobTitle) return err("userId and jobTitle are required", "MISSING_PARAM");
47476
- const { error: error2 } = await supabase.from("users").update({ job_title: jobTitle, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", userId);
47575
+ const rawTitle = params.jobTitle;
47576
+ if (!userId || !rawTitle) return err("userId and jobTitle are required", "MISSING_PARAM");
47577
+ const titles = Array.isArray(rawTitle) ? rawTitle : [rawTitle];
47578
+ if (titles.length === 0) return err("At least one role is required", "INVALID_PARAM");
47579
+ const valid = ["product_lead", "engineer", "designer", "leadership", "sales", "member"];
47580
+ const invalid = titles.filter((t) => !valid.includes(t));
47581
+ if (invalid.length > 0) return err(`Invalid role(s): ${invalid.join(", ")}`, "INVALID_PARAM");
47582
+ const { error: error2 } = await supabase.from("users").update({ job_title: titles, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", userId);
47477
47583
  if (error2) return err(error2.message);
47478
- return ok({ userId, jobTitle }, `Updated role to ${jobTitle}`);
47584
+ return ok({ userId, jobTitle: titles }, `Updated roles to ${titles.join(", ")}`);
47479
47585
  }
47480
47586
  case "assign_pod": {
47481
47587
  const userId = params.userId;
@@ -47486,8 +47592,8 @@ async function handleTeam(params) {
47486
47592
  const { data: user } = await supabase.from("users").select("full_name, job_title").eq("id", userId).single();
47487
47593
  const displayName = user?.full_name || "Unknown";
47488
47594
  const initials = displayName.split(" ").map((w) => w[0]).join("").toUpperCase().slice(0, 2);
47489
- const roleMap = { product_lead: "product_lead", engineer: "engineer", designer: "designer" };
47490
- const podRole = roleMap[user?.job_title || ""] || "engineer";
47595
+ const jobTitles = user?.job_title || ["member"];
47596
+ const podRole = jobTitles.includes("product_lead") ? "product_lead" : jobTitles.includes("designer") ? "designer" : "engineer";
47491
47597
  await supabase.from("pod_members").insert({
47492
47598
  pod_id: podId,
47493
47599
  user_id: userId,
@@ -47531,6 +47637,7 @@ server.tool(
47531
47637
  action: external_exports4.enum(["list", "add", "update", "remove"]),
47532
47638
  podId: external_exports4.string().describe("Pod ID"),
47533
47639
  memberId: external_exports4.string().optional().describe("Member ID (for update/remove)"),
47640
+ userId: external_exports4.string().optional().describe("User ID \u2014 links pod member to a user (for add). Auto-populates name/initials/role from user."),
47534
47641
  displayName: external_exports4.string().optional().describe("Display name"),
47535
47642
  initials: external_exports4.string().optional().describe("2-letter initials"),
47536
47643
  role: external_exports4.string().optional().describe("Role: engineer, product_lead, designer")
@@ -47539,7 +47646,7 @@ server.tool(
47539
47646
  );
47540
47647
  server.tool(
47541
47648
  "pulse_features",
47542
- "Manage features and tasks. Actions: list (filter by pod/workstream/week/status/size), get, create (feature or task via size param), update (phase/step/status/blocker/week), delete, add_note, reorder.",
47649
+ "Manage features and tasks. Actions: list (filter by pod/workstream/week/status/size), get, create (feature or task via size param), update (phase/step/status/blocker/week/prdText/prdFileUrl), delete, add_note, reorder.",
47543
47650
  {
47544
47651
  action: external_exports4.enum(["list", "get", "create", "update", "delete", "add_note", "reorder"]),
47545
47652
  featureId: external_exports4.string().optional().describe("Feature ID"),
@@ -47556,6 +47663,9 @@ server.tool(
47556
47663
  blockerText: external_exports4.string().optional().describe("Blocker description"),
47557
47664
  sortOrder: external_exports4.number().optional().describe("Sort order for drag-to-reorder"),
47558
47665
  projectColor: external_exports4.string().optional().describe("Project color hex"),
47666
+ prdText: external_exports4.string().optional().describe("PRD text content (for update)"),
47667
+ prdFileUrl: external_exports4.string().optional().describe("PRD file URL (for update)"),
47668
+ prdFileName: external_exports4.string().optional().describe("PRD file name (for update)"),
47559
47669
  note: external_exports4.string().optional().describe("Note text (for add_note)"),
47560
47670
  items: external_exports4.array(external_exports4.object({ id: external_exports4.string(), sortOrder: external_exports4.number() })).optional().describe("Items to reorder"),
47561
47671
  limit: external_exports4.number().optional().describe("Max results")
@@ -47671,7 +47781,7 @@ server.tool(
47671
47781
  {
47672
47782
  action: external_exports4.enum(["list", "update_role", "assign_pod"]),
47673
47783
  userId: external_exports4.string().optional().describe("User ID"),
47674
- jobTitle: external_exports4.string().optional().describe("Job title: product_lead, engineer, designer, leadership, sales, member"),
47784
+ jobTitle: external_exports4.union([external_exports4.string(), external_exports4.array(external_exports4.string())]).optional().describe("Job title(s): product_lead, engineer, designer, leadership, sales, member. Accepts single string or array."),
47675
47785
  podId: external_exports4.string().optional().nullable().describe("Pod ID (null to remove from pods)")
47676
47786
  },
47677
47787
  async (params) => handleTeam(params)
@@ -47679,6 +47789,8 @@ server.tool(
47679
47789
  async function main() {
47680
47790
  const transport = new StdioServerTransport();
47681
47791
  await server.connect(transport);
47792
+ getCurrentUser().catch(() => {
47793
+ });
47682
47794
  console.error("Pulse MCP server running");
47683
47795
  }
47684
47796
  main().catch((e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vailent/pulse-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Pulse MCP server — manage pods, features, workstreams, bugs, and more from Claude Code",
5
5
  "type": "module",
6
6
  "bin": {