@sideboard-ai/core 0.1.190 → 0.1.191

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.
@@ -15193,13 +15193,17 @@ var init_home_board = __esm({
15193
15193
  });
15194
15194
 
15195
15195
  // src/integrations/abletime-mcp.ts
15196
+ function abletimeToolUsesPm(name) {
15197
+ return ABLETIME_PM_TOOL_NAMES.includes(name);
15198
+ }
15196
15199
  function normalizeAbleTimeHost(raw) {
15197
15200
  const trimmed = raw?.trim() || DEFAULT_ABLETIME_HOST;
15198
15201
  const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
15199
15202
  return withScheme.replace(/\/+$/, "");
15200
15203
  }
15201
- function abletimeMcpUrl(host) {
15202
- return `${normalizeAbleTimeHost(host)}${ABLETIME_MCP_PATH}`;
15204
+ function abletimeMcpUrl(host, opts) {
15205
+ const path = opts?.pm ? ABLETIME_MCP_PM_PATH : ABLETIME_MCP_PATH;
15206
+ return `${normalizeAbleTimeHost(host)}${path}`;
15203
15207
  }
15204
15208
  function rewriteAbleTimeError(message) {
15205
15209
  if (/INTEGRATION_AGENT_ACCESS_DISABLED|agent access/i.test(message)) {
@@ -15277,7 +15281,8 @@ async function abletimeMcpRequest(method, params, opts) {
15277
15281
  throw new Error("AbleTime is not connected \u2014 paste a personal access token in Account settings");
15278
15282
  }
15279
15283
  const url = abletimeMcpUrl(
15280
- opts?.host ?? (opts?.token ? DEFAULT_ABLETIME_HOST : getAbleTimeHost())
15284
+ opts?.host ?? (opts?.token ? DEFAULT_ABLETIME_HOST : getAbleTimeHost()),
15285
+ { pm: opts?.pm }
15281
15286
  );
15282
15287
  const res = await httpFetch(url, {
15283
15288
  method: "POST",
@@ -15341,7 +15346,8 @@ async function callAbleTimeTool(name, args = {}, opts) {
15341
15346
  if (!opts?.token && !isAbleTimeConnected()) {
15342
15347
  throw new Error("AbleTime is not connected \u2014 paste a personal access token in Account settings");
15343
15348
  }
15344
- const call = () => abletimeMcpRequest("tools/call", { name, arguments: args }, opts).then((result) => {
15349
+ const requestOpts = { ...opts, pm: opts?.pm ?? abletimeToolUsesPm(name) };
15350
+ const call = () => abletimeMcpRequest("tools/call", { name, arguments: args }, requestOpts).then((result) => {
15345
15351
  return unwrapToolResult(result);
15346
15352
  });
15347
15353
  try {
@@ -15349,13 +15355,13 @@ async function callAbleTimeTool(name, args = {}, opts) {
15349
15355
  } catch (err) {
15350
15356
  const message = err instanceof Error ? err.message : String(err);
15351
15357
  if (/initialize|session|not initialized|-32000/i.test(message)) {
15352
- await initializeIfNeeded(opts);
15358
+ await initializeIfNeeded(requestOpts);
15353
15359
  return await call();
15354
15360
  }
15355
15361
  throw err instanceof Error ? new Error(rewriteAbleTimeError(err.message)) : err;
15356
15362
  }
15357
15363
  }
15358
- var DEFAULT_ABLETIME_HOST, ABLETIME_MCP_PATH, MCP_PROTOCOL_VERSION;
15364
+ var DEFAULT_ABLETIME_HOST, ABLETIME_MCP_PATH, ABLETIME_MCP_PM_PATH, MCP_PROTOCOL_VERSION, ABLETIME_PM_TOOL_NAMES;
15359
15365
  var init_abletime_mcp = __esm({
15360
15366
  "src/integrations/abletime-mcp.ts"() {
15361
15367
  "use strict";
@@ -15363,13 +15369,27 @@ var init_abletime_mcp = __esm({
15363
15369
  init_app_settings();
15364
15370
  DEFAULT_ABLETIME_HOST = "https://track.abletime.com";
15365
15371
  ABLETIME_MCP_PATH = "/api/public/v2/mcp";
15372
+ ABLETIME_MCP_PM_PATH = "/api/public/v2/mcp/pm";
15366
15373
  MCP_PROTOCOL_VERSION = "2025-03-26";
15374
+ ABLETIME_PM_TOOL_NAMES = [
15375
+ "set_task_assignee",
15376
+ "set_task_priority",
15377
+ "set_task_stage",
15378
+ "set_task_dependency",
15379
+ "set_task_blocked",
15380
+ "set_task_locked",
15381
+ "set_task_schedule",
15382
+ "clear_task_schedule",
15383
+ "archive_task",
15384
+ "delete_task"
15385
+ ];
15367
15386
  }
15368
15387
  });
15369
15388
 
15370
15389
  // src/integrations/abletime.ts
15371
15390
  var abletime_exports = {};
15372
15391
  __export(abletime_exports, {
15392
+ ableTimeDependencyTarget: () => ableTimeDependencyTarget,
15373
15393
  commentAbleTimeTask: () => commentAbleTimeTask,
15374
15394
  createAbleTimeTask: () => createAbleTimeTask,
15375
15395
  ensureAbleTimeTask: () => ensureAbleTimeTask,
@@ -15626,39 +15646,122 @@ async function commentAbleTimeTask(input, opts) {
15626
15646
  body: rec ? firstString(rec, ["body", "text", "comment"]) || body : body
15627
15647
  };
15628
15648
  }
15649
+ function isAbleTimeNoneToken(value) {
15650
+ if (value == null) return true;
15651
+ const s = value.trim();
15652
+ return !s || /^(none|clear|unschedule|null)$/i.test(s);
15653
+ }
15654
+ function taskIdArgs(id) {
15655
+ return { id, task_id: id, task: id };
15656
+ }
15657
+ function ableTimeDependencyTarget(thisId, type, relatedId) {
15658
+ const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
15659
+ if (s === "none" || s === "clear" || s === "null") {
15660
+ return { taskId: thisId, dependsOn: null };
15661
+ }
15662
+ if (s === "blockedby" || s === "blocked" || s === "dependson" || s === "waiton") {
15663
+ return { taskId: thisId, dependsOn: relatedId };
15664
+ }
15665
+ if (s === "blocks" || s === "blocking") {
15666
+ return { taskId: relatedId, dependsOn: thisId };
15667
+ }
15668
+ throw new Error(
15669
+ `AbleTime relation type must be blocks or blockedBy (got ${type}). AbleTime models one wait-on dependency.`
15670
+ );
15671
+ }
15672
+ async function setAbleTimeTaskDependency(taskId, dependsOn, opts) {
15673
+ const args = {
15674
+ ...taskIdArgs(taskId)
15675
+ };
15676
+ if (dependsOn) {
15677
+ args.depends_on = dependsOn;
15678
+ args.depends_on_id = dependsOn;
15679
+ args.dependency = dependsOn;
15680
+ args.dependency_id = dependsOn;
15681
+ args.blocked_by = dependsOn;
15682
+ args.wait_on = dependsOn;
15683
+ args.related_task_id = dependsOn;
15684
+ } else {
15685
+ args.depends_on = null;
15686
+ args.dependency = null;
15687
+ args.blocked_by = null;
15688
+ args.clear = true;
15689
+ }
15690
+ await callAbleTimeTool("set_task_dependency", args, opts);
15691
+ }
15629
15692
  async function updateAbleTimeTask(input, opts) {
15630
15693
  const id = input.id.trim();
15631
15694
  if (!id) throw new Error("AbleTime task id is required");
15632
15695
  const title = input.title?.trim();
15633
15696
  const description = input.description;
15634
15697
  const state = input.state?.trim();
15635
- if (!title && description === void 0 && !state) {
15636
- throw new Error("abletime_update_task needs at least one of title, description, state");
15698
+ const addRelations = input.relations?.length ? input.relations : void 0;
15699
+ const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
15700
+ const updateArgs = { ...taskIdArgs(id) };
15701
+ if (title) updateArgs.title = title;
15702
+ if (description !== void 0) updateArgs.description = description;
15703
+ if (input.labels !== void 0) {
15704
+ updateArgs.tags = input.labels;
15705
+ updateArgs.labels = input.labels;
15706
+ updateArgs.tag_names = input.labels;
15707
+ }
15708
+ if (input.project !== void 0) {
15709
+ const projectName = (input.project ?? "").trim();
15710
+ if (isAbleTimeNoneToken(projectName)) {
15711
+ throw new Error("AbleTime tasks stay in a project \u2014 pass a project name or id");
15712
+ }
15713
+ const project = await resolveAbleTimeProject(projectName, opts);
15714
+ updateArgs.project = project.id;
15715
+ updateArgs.project_id = project.id;
15716
+ }
15717
+ if (input.parent !== void 0) {
15718
+ if (isAbleTimeNoneToken(input.parent)) {
15719
+ updateArgs.parent = null;
15720
+ updateArgs.parent_id = null;
15721
+ updateArgs.related_task_id = null;
15722
+ } else {
15723
+ const parent = (input.parent ?? "").trim();
15724
+ updateArgs.parent = parent;
15725
+ updateArgs.parent_id = parent;
15726
+ updateArgs.related_task_id = parent;
15727
+ }
15728
+ }
15729
+ const hasUpdateFields = Boolean(title) || description !== void 0 || input.labels !== void 0 || input.project !== void 0 || input.parent !== void 0;
15730
+ if (!hasUpdateFields && !state && !addRelations && !removeRelations) {
15731
+ throw new Error(
15732
+ "abletime_update_task needs at least one of title, description, state, labels, project, parent, relations, removeRelations"
15733
+ );
15637
15734
  }
15638
15735
  if (state) {
15639
15736
  await callAbleTimeTool(
15640
15737
  "set_task_state",
15641
- { id, task_id: id, task: id, state },
15738
+ { ...taskIdArgs(id), state },
15642
15739
  opts
15643
15740
  ).catch(async () => {
15644
15741
  await callAbleTimeTool(
15645
15742
  "update_task",
15646
- { id, task_id: id, title, description, state },
15743
+ { ...updateArgs, state },
15647
15744
  opts
15648
15745
  );
15649
15746
  });
15650
15747
  }
15651
- if (title || description !== void 0) {
15652
- await callAbleTimeTool(
15653
- "update_task",
15654
- {
15655
- id,
15656
- task_id: id,
15657
- ...title ? { title } : {},
15658
- ...description !== void 0 ? { description } : {}
15659
- },
15660
- opts
15661
- );
15748
+ if (hasUpdateFields) {
15749
+ await callAbleTimeTool("update_task", updateArgs, opts);
15750
+ }
15751
+ for (const rel of removeRelations ?? []) {
15752
+ const related = rel.issue.trim() || id;
15753
+ const target = ableTimeDependencyTarget(id, rel.type, related);
15754
+ await setAbleTimeTaskDependency(target.taskId, null, opts);
15755
+ }
15756
+ for (const rel of addRelations ?? []) {
15757
+ const related = rel.issue.trim();
15758
+ if (!related) throw new Error("AbleTime relations.issue is required (id or CRM-232)");
15759
+ const target = ableTimeDependencyTarget(id, rel.type, related);
15760
+ if (!target.dependsOn) {
15761
+ await setAbleTimeTaskDependency(target.taskId, null, opts);
15762
+ continue;
15763
+ }
15764
+ await setAbleTimeTaskDependency(target.taskId, target.dependsOn, opts);
15662
15765
  }
15663
15766
  return getAbleTimeTask(id, opts);
15664
15767
  }
@@ -18307,17 +18410,17 @@ function formatIssueToolsDirective(opts) {
18307
18410
  ];
18308
18411
  if (opts.linear) {
18309
18412
  lines.push(
18310
- "- Linear: `linear_search_issues` (assignee=me; pass `updatedSince` for \u201Cany updates since yesterday?\u201D \u2014 new tickets, edits, and comments), `linear_get_issue` (comments; pass `include=full` if crush.truncated), `linear_comment`, `linear_update_issue` (state, cycle), `linear_create_issue` (pass `parent` for spin-offs; call `linear_list_teams` first). Scope errors: reconnect Linear in Settings \u2192 Issues."
18413
+ "- Linear: `linear_search_issues` (assignee=me; pass `updatedSince` for \u201Cany updates since yesterday?\u201D \u2014 new tickets, edits, and comments), `linear_get_issue` (comments; pass `include=full` if crush.truncated), `linear_comment`, `linear_update_issue` (state, cycle, labels, project, parent, relations), `linear_create_issue` (pass `parent` for spin-offs; call `linear_list_teams` first). Scope errors: reconnect Linear in Settings \u2192 Issues."
18311
18414
  );
18312
18415
  }
18313
18416
  if (github) {
18314
18417
  lines.push(
18315
- "- GitHub: `github_search_issues` (assignee=me; `updatedSince` for inbox updates + comments), `github_get_issue` (comments; pass `include=full` if crush.truncated), `github_comment`, `github_update_issue` (state open|closed), `github_create_issue` (pass `parent` for spin-offs). Uses Account `gh`."
18418
+ "- GitHub: `github_search_issues` (assignee=me; `updatedSince` for inbox updates + comments), `github_get_issue` (comments; pass `include=full` if crush.truncated), `github_comment`, `github_update_issue` (state open|closed, labels, project, parent, relations), `github_create_issue` (pass `parent` for spin-offs). Uses Account `gh`."
18316
18419
  );
18317
18420
  }
18318
18421
  if (opts.abletime) {
18319
18422
  lines.push(
18320
- "- AbleTime: `abletime_list_tasks` / `abletime_search_tasks` (`updatedSince` for inbox updates + comments), `abletime_get_task` (comments; pass `include=full` if crush.truncated), `abletime_comment`, `abletime_update_task` (state), `abletime_create_task` (pass `parent` for spin-offs; `abletime_list_projects` if needed)."
18423
+ "- AbleTime: `abletime_list_tasks` / `abletime_search_tasks` (`updatedSince` for inbox updates + comments), `abletime_get_task` (comments; pass `include=full` if crush.truncated), `abletime_comment`, `abletime_update_task` (state, labels, project, parent, relations), `abletime_create_task` (pass `parent` for spin-offs; `abletime_list_projects` if needed)."
18321
18424
  );
18322
18425
  }
18323
18426
  lines.push(
@@ -21903,13 +22006,78 @@ async function commentGitHubIssue(input, opts) {
21903
22006
  const url = stdout.trim().split(/\s+/).find((part) => /^https?:\/\//i.test(part));
21904
22007
  return { body, url };
21905
22008
  }
22009
+ function isGitHubNoneToken(value) {
22010
+ if (value == null) return true;
22011
+ const s = value.trim();
22012
+ return !s || /^(none|clear|unschedule|null)$/i.test(s);
22013
+ }
22014
+ function githubRelationEditFlag(type, remove = false) {
22015
+ const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
22016
+ if (s === "blockedby" || s === "blocked" || s === "dependson") {
22017
+ return remove ? "--remove-blocked-by" : "--add-blocked-by";
22018
+ }
22019
+ if (s === "blocks" || s === "blocking") {
22020
+ return remove ? "--remove-blocking" : "--add-blocking";
22021
+ }
22022
+ throw new Error(
22023
+ `GitHub relation type must be blocks or blockedBy (got ${type}). Parent is a separate parent= field.`
22024
+ );
22025
+ }
22026
+ function labelsEqual(a, b) {
22027
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
22028
+ }
22029
+ function parseProjectTitles(raw) {
22030
+ const out = [];
22031
+ const seen = /* @__PURE__ */ new Set();
22032
+ const push = (name) => {
22033
+ const title = name.trim();
22034
+ const key = title.toLowerCase();
22035
+ if (!title || seen.has(key)) return;
22036
+ seen.add(key);
22037
+ out.push(title);
22038
+ };
22039
+ const fromItems = raw.projectItems ?? raw.projects ?? raw.projectItemsV2;
22040
+ if (Array.isArray(fromItems)) {
22041
+ for (const item of fromItems) {
22042
+ if (typeof item === "string") {
22043
+ push(item);
22044
+ continue;
22045
+ }
22046
+ if (!item || typeof item !== "object") continue;
22047
+ const rec = item;
22048
+ const nested = rec.project && typeof rec.project === "object" ? rec.project : rec;
22049
+ const title = String(nested.title ?? nested.name ?? rec.title ?? rec.name ?? "").trim();
22050
+ if (title) push(title);
22051
+ }
22052
+ }
22053
+ return out;
22054
+ }
22055
+ async function listGitHubIssueProjects(number, cwd, repoArgs) {
22056
+ const result = await gh(
22057
+ ["issue", "view", String(number), ...repoArgs, "--json", "projectItems,projects"],
22058
+ cwd,
22059
+ { reject: false }
22060
+ );
22061
+ if (result.exitCode !== 0 || !result.stdout.trim()) return [];
22062
+ try {
22063
+ const parsed = JSON.parse(result.stdout);
22064
+ return parseProjectTitles(parsed);
22065
+ } catch {
22066
+ return [];
22067
+ }
22068
+ }
21906
22069
  async function updateGitHubIssue(input, opts) {
21907
22070
  const number = parseGitHubIssueNumber(input.id);
21908
22071
  const title = input.title?.trim();
21909
22072
  const body = input.body;
21910
22073
  const state = input.state?.trim().toLowerCase();
21911
- if (!title && body === void 0 && !state) {
21912
- throw new Error("github_update_issue needs at least one of title, body, state");
22074
+ const addRelations = input.relations?.length ? input.relations : void 0;
22075
+ const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
22076
+ const hasEditFields = Boolean(title) || body !== void 0 || input.labels !== void 0 || input.project !== void 0 || input.parent !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
22077
+ if (!hasEditFields && !state) {
22078
+ throw new Error(
22079
+ "github_update_issue needs at least one of title, body, state, labels, project, parent, relations, removeRelations"
22080
+ );
21913
22081
  }
21914
22082
  const { cwd, repoArgs } = await resolveGitHubIssueRepo(opts?.repoPath);
21915
22083
  if (state === "closed" || state === "close") {
@@ -21925,10 +22093,46 @@ async function updateGitHubIssue(input, opts) {
21925
22093
  } else if (state) {
21926
22094
  throw new Error(`GitHub issue state must be open or closed (got ${input.state})`);
21927
22095
  }
21928
- if (title || body !== void 0) {
21929
- const args = ["issue", "edit", String(number), ...repoArgs];
21930
- if (title) args.push("--title", title);
21931
- if (body !== void 0) args.push("--body", body);
22096
+ const args = ["issue", "edit", String(number), ...repoArgs];
22097
+ if (title) args.push("--title", title);
22098
+ if (body !== void 0) args.push("--body", body);
22099
+ if (input.labels !== void 0) {
22100
+ const existing = await getGitHubIssue(String(number), opts);
22101
+ const desired = input.labels.map((name) => name.trim()).filter(Boolean);
22102
+ const add = desired.filter((name) => !existing.labels.some((cur) => labelsEqual(cur, name)));
22103
+ const remove = existing.labels.filter((cur) => !desired.some((name) => labelsEqual(cur, name)));
22104
+ if (add.length) args.push("--add-label", add.join(","));
22105
+ if (remove.length) args.push("--remove-label", remove.join(","));
22106
+ }
22107
+ if (input.project !== void 0) {
22108
+ const current = await listGitHubIssueProjects(number, cwd, repoArgs);
22109
+ if (isGitHubNoneToken(input.project)) {
22110
+ for (const name of current) args.push("--remove-project", name);
22111
+ } else {
22112
+ const wanted = (input.project ?? "").trim();
22113
+ if (!current.some((name) => labelsEqual(name, wanted))) {
22114
+ args.push("--add-project", wanted);
22115
+ }
22116
+ for (const name of current) {
22117
+ if (!labelsEqual(name, wanted)) args.push("--remove-project", name);
22118
+ }
22119
+ }
22120
+ }
22121
+ if (input.parent !== void 0) {
22122
+ if (isGitHubNoneToken(input.parent)) {
22123
+ args.push("--remove-parent");
22124
+ } else {
22125
+ args.push("--parent", String(parseGitHubIssueNumber(input.parent ?? "")));
22126
+ }
22127
+ }
22128
+ for (const rel of removeRelations ?? []) {
22129
+ args.push(githubRelationEditFlag(rel.type, true), String(parseGitHubIssueNumber(rel.issue)));
22130
+ }
22131
+ for (const rel of addRelations ?? []) {
22132
+ args.push(githubRelationEditFlag(rel.type, false), String(parseGitHubIssueNumber(rel.issue)));
22133
+ }
22134
+ const editChanged = args.length > 3 + repoArgs.length;
22135
+ if (editChanged) {
21932
22136
  requireGhOk(await gh(args, cwd, { reject: false }), `GitHub edit #${number}`);
21933
22137
  }
21934
22138
  return getGitHubIssue(String(number), opts);
@@ -22105,12 +22309,14 @@ var ISSUE_FIELDS = `
22105
22309
  children(first: 25) { nodes { ${ISSUE_REF_FIELDS} } }
22106
22310
  relations(first: 25) {
22107
22311
  nodes {
22312
+ id
22108
22313
  type
22109
22314
  relatedIssue { ${ISSUE_REF_FIELDS} }
22110
22315
  }
22111
22316
  }
22112
22317
  inverseRelations(first: 25) {
22113
22318
  nodes {
22319
+ id
22114
22320
  type
22115
22321
  issue { ${ISSUE_REF_FIELDS} }
22116
22322
  }
@@ -22230,6 +22436,35 @@ mutation SideboardCommentCreate($input: CommentCreateInput!) {
22230
22436
  }
22231
22437
  }
22232
22438
  `;
22439
+ var LABELS_QUERY = `
22440
+ query SideboardLabels($first: Int!) {
22441
+ issueLabels(first: $first) {
22442
+ nodes { id name }
22443
+ }
22444
+ }
22445
+ `;
22446
+ var PROJECTS_QUERY = `
22447
+ query SideboardProjects($first: Int!, $filter: ProjectFilter) {
22448
+ projects(first: $first, filter: $filter) {
22449
+ nodes { id name slugId }
22450
+ }
22451
+ }
22452
+ `;
22453
+ var ISSUE_RELATION_CREATE = `
22454
+ mutation SideboardIssueRelationCreate($input: IssueRelationCreateInput!) {
22455
+ issueRelationCreate(input: $input) {
22456
+ success
22457
+ issueRelation { id type }
22458
+ }
22459
+ }
22460
+ `;
22461
+ var ISSUE_RELATION_DELETE = `
22462
+ mutation SideboardIssueRelationDelete($id: String!) {
22463
+ issueRelationDelete(id: $id) {
22464
+ success
22465
+ }
22466
+ }
22467
+ `;
22233
22468
  function rewriteLinearError(message) {
22234
22469
  if (/scope|permission|not (authorized|allowed)|unauthorized|insufficient/i.test(message)) {
22235
22470
  return `${message} \u2014 Disconnect and Connect Linear in Account settings to grant write access.`;
@@ -22349,12 +22584,18 @@ function mapRelations(node) {
22349
22584
  for (const rel of node.relations?.nodes ?? []) {
22350
22585
  const issue = mapIssueRef(rel.relatedIssue);
22351
22586
  if (!issue) continue;
22352
- out.push({ type: String(rel.type ?? "related"), issue });
22587
+ const id = rel.id?.trim();
22588
+ out.push({ type: String(rel.type ?? "related"), issue, ...id ? { id } : {} });
22353
22589
  }
22354
22590
  for (const rel of node.inverseRelations?.nodes ?? []) {
22355
22591
  const issue = mapIssueRef(rel.issue);
22356
22592
  if (!issue) continue;
22357
- out.push({ type: flipLinearRelationType(String(rel.type ?? "related")), issue });
22593
+ const id = rel.id?.trim();
22594
+ out.push({
22595
+ type: flipLinearRelationType(String(rel.type ?? "related")),
22596
+ issue,
22597
+ ...id ? { id } : {}
22598
+ });
22358
22599
  }
22359
22600
  return out;
22360
22601
  }
@@ -22515,6 +22756,148 @@ function resolveLinearCycle(team, cycle) {
22515
22756
  }
22516
22757
  return found.id;
22517
22758
  }
22759
+ var LINEAR_NONE_RE = /^(none|clear|unschedule|null)$/i;
22760
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
22761
+ function isLinearNoneToken(value) {
22762
+ if (value == null) return true;
22763
+ const s = value.trim();
22764
+ return !s || LINEAR_NONE_RE.test(s);
22765
+ }
22766
+ function resolveLinearLabel(labels, name) {
22767
+ const s = name.trim();
22768
+ if (!s) throw new Error("Linear label is empty");
22769
+ const lower = s.toLowerCase();
22770
+ const found = labels.find((x) => x.id === s) || labels.find((x) => x.name.toLowerCase() === lower);
22771
+ if (!found) {
22772
+ const available = labels.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
22773
+ throw new Error(`Linear label not found: ${name}. Available: ${available}`);
22774
+ }
22775
+ return found;
22776
+ }
22777
+ function resolveLinearProject(projects, project) {
22778
+ if (isLinearNoneToken(project)) return null;
22779
+ const s = (project ?? "").trim();
22780
+ const lower = s.toLowerCase();
22781
+ const found = projects.find((x) => x.id === s) || projects.find((x) => x.name.toLowerCase() === lower) || projects.find((x) => x.slugId?.toLowerCase() === lower);
22782
+ if (!found) {
22783
+ const available = projects.map((x) => x.name).filter(Boolean).slice(0, 20).join(", ") || "(none)";
22784
+ throw new Error(`Linear project not found: ${project}. Available: ${available}`);
22785
+ }
22786
+ return found.id;
22787
+ }
22788
+ function normalizeLinearRelationType(type) {
22789
+ const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
22790
+ if (s === "blocks" || s === "block") return "blocks";
22791
+ if (s === "blockedby" || s === "blocked") return "blockedBy";
22792
+ if (s === "related" || s === "relates" || s === "relatesto") return "related";
22793
+ if (s === "duplicate" || s === "dup") return "duplicate";
22794
+ if (s === "duplicateof" || s === "original") return "duplicateOf";
22795
+ if (s === "similar") return "similar";
22796
+ throw new Error(
22797
+ `Linear relation type must be blocks, blockedBy, related, duplicate, or duplicateOf (got ${type})`
22798
+ );
22799
+ }
22800
+ function linearRelationCreateInput(thisIssueId, type, relatedIssueId) {
22801
+ const view = normalizeLinearRelationType(type);
22802
+ if (view === "blockedBy") {
22803
+ return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "blocks" };
22804
+ }
22805
+ if (view === "duplicateOf") {
22806
+ return { issueId: relatedIssueId, relatedIssueId: thisIssueId, type: "duplicate" };
22807
+ }
22808
+ return { issueId: thisIssueId, relatedIssueId, type: view };
22809
+ }
22810
+ function relationMatches(rel, type, issueRef) {
22811
+ if (rel.type.toLowerCase() !== type.toLowerCase()) return false;
22812
+ const ref = issueRef.trim().toLowerCase();
22813
+ return rel.issue.id.toLowerCase() === ref || rel.issue.identifier.toLowerCase() === ref;
22814
+ }
22815
+ function formatRelationList(relations) {
22816
+ if (!relations.length) return "(none)";
22817
+ return relations.map((r) => `${r.type} ${r.issue.identifier || r.issue.id}`).join(", ");
22818
+ }
22819
+ async function listLinearLabels(opts) {
22820
+ const json = await linearGraphql(LABELS_QUERY, { first: 250 }, opts);
22821
+ return (json.issueLabels?.nodes ?? []).flatMap((node) => {
22822
+ const id = String(node.id ?? "").trim();
22823
+ const name = String(node.name ?? "").trim();
22824
+ if (!id) return [];
22825
+ return [{ id, name }];
22826
+ });
22827
+ }
22828
+ async function listLinearProjects(filter, opts) {
22829
+ const json = await linearGraphql(PROJECTS_QUERY, { first: 50, filter: filter ?? {} }, opts);
22830
+ return (json.projects?.nodes ?? []).flatMap((node) => {
22831
+ const id = String(node.id ?? "").trim();
22832
+ if (!id) return [];
22833
+ return [
22834
+ {
22835
+ id,
22836
+ name: String(node.name ?? ""),
22837
+ ...node.slugId ? { slugId: String(node.slugId) } : {}
22838
+ }
22839
+ ];
22840
+ });
22841
+ }
22842
+ async function resolveLinearProjectId(project, opts) {
22843
+ if (isLinearNoneToken(project)) return null;
22844
+ const s = (project ?? "").trim();
22845
+ const filter = UUID_RE.test(s) ? { id: { eq: s } } : { name: { eqIgnoreCase: s } };
22846
+ const matched = await listLinearProjects(filter, opts);
22847
+ try {
22848
+ return resolveLinearProject(matched, s);
22849
+ } catch {
22850
+ if (UUID_RE.test(s)) return s;
22851
+ const listed = await listLinearProjects(void 0, opts);
22852
+ return resolveLinearProject(listed, s);
22853
+ }
22854
+ }
22855
+ async function resolveLinearLabelIds(names, opts) {
22856
+ if (names.length === 0) return [];
22857
+ const labels = await listLinearLabels(opts);
22858
+ return names.map((name) => {
22859
+ const s = name.trim();
22860
+ try {
22861
+ return resolveLinearLabel(labels, s).id;
22862
+ } catch (error) {
22863
+ if (UUID_RE.test(s)) return s;
22864
+ throw error;
22865
+ }
22866
+ });
22867
+ }
22868
+ async function applyLinearIssueRelations(issue, add, remove, opts) {
22869
+ const current = [...issue.relations];
22870
+ for (const rel of remove ?? []) {
22871
+ const type = normalizeLinearRelationType(rel.type);
22872
+ const issueRef = rel.issue.trim();
22873
+ if (!issueRef) throw new Error("Linear removeRelations.issue is required (uuid or ENG-123)");
22874
+ const match = current.find((r) => relationMatches(r, type, issueRef));
22875
+ if (!match?.id) {
22876
+ throw new Error(
22877
+ `Linear relation not found: ${type} ${issueRef}. Existing: ${formatRelationList(current)}`
22878
+ );
22879
+ }
22880
+ const json = await linearGraphql(ISSUE_RELATION_DELETE, { id: match.id }, opts);
22881
+ if (!json.issueRelationDelete?.success) {
22882
+ throw new Error(`Linear issueRelationDelete failed (${type} ${issueRef})`);
22883
+ }
22884
+ const idx = current.indexOf(match);
22885
+ if (idx >= 0) current.splice(idx, 1);
22886
+ }
22887
+ for (const rel of add ?? []) {
22888
+ const type = normalizeLinearRelationType(rel.type);
22889
+ const issueRef = rel.issue.trim();
22890
+ if (!issueRef) throw new Error("Linear relations.issue is required (uuid or ENG-123)");
22891
+ if (current.some((r) => relationMatches(r, type, issueRef))) continue;
22892
+ const related = await getLinearIssue(issueRef, opts);
22893
+ const input = linearRelationCreateInput(issue.id, type, related.id);
22894
+ const json = await linearGraphql(ISSUE_RELATION_CREATE, { input }, opts);
22895
+ if (!json.issueRelationCreate?.success) {
22896
+ throw new Error(`Linear issueRelationCreate failed (${type} ${related.identifier})`);
22897
+ }
22898
+ current.push({ type, issue: related, id: void 0 });
22899
+ }
22900
+ }
22518
22901
  function normalizePriority(priority) {
22519
22902
  if (priority == null) return void 0;
22520
22903
  if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
@@ -22522,7 +22905,6 @@ function normalizePriority(priority) {
22522
22905
  }
22523
22906
  return priority;
22524
22907
  }
22525
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
22526
22908
  function assigneeFilterKey(assignee) {
22527
22909
  return (assignee ?? "").trim();
22528
22910
  }
@@ -22729,8 +23111,10 @@ async function updateLinearIssue(input, opts) {
22729
23111
  if (input.description !== void 0) {
22730
23112
  mutationInput.description = input.description;
22731
23113
  }
22732
- const needsTeam = Boolean(input.state?.trim()) || input.cycle !== void 0;
22733
- const existing = needsTeam ? await getLinearIssue(issueId, opts) : void 0;
23114
+ const addRelations = input.relations?.length ? input.relations : void 0;
23115
+ const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
23116
+ const needsIssue = Boolean(input.state?.trim()) || input.cycle !== void 0 || Boolean(addRelations) || Boolean(removeRelations);
23117
+ const existing = needsIssue ? await getLinearIssue(issueId, opts) : void 0;
22734
23118
  if (input.state?.trim()) {
22735
23119
  const { teams } = await listLinearTeams(opts);
22736
23120
  const teamRef = existing?.team?.key || existing?.team?.id;
@@ -22763,14 +23147,44 @@ async function updateLinearIssue(input, opts) {
22763
23147
  if (input.priority !== void 0) {
22764
23148
  mutationInput.priority = normalizePriority(input.priority);
22765
23149
  }
22766
- if (Object.keys(mutationInput).length === 0) {
22767
- throw new Error("linear_update_issue needs at least one of title, description, state, assignee, priority, cycle");
23150
+ if (input.labels !== void 0) {
23151
+ mutationInput.labelIds = await resolveLinearLabelIds(input.labels, opts);
23152
+ }
23153
+ if (input.project !== void 0) {
23154
+ mutationInput.projectId = await resolveLinearProjectId(input.project, opts);
23155
+ }
23156
+ if (input.parent !== void 0) {
23157
+ if (isLinearNoneToken(input.parent)) {
23158
+ mutationInput.parentId = null;
23159
+ } else {
23160
+ const parent = await getLinearIssue((input.parent ?? "").trim(), opts);
23161
+ mutationInput.parentId = parent.id;
23162
+ }
23163
+ }
23164
+ const hasIssueFields = Object.keys(mutationInput).length > 0;
23165
+ const hasRelationWrites = Boolean(addRelations || removeRelations);
23166
+ if (!hasIssueFields && !hasRelationWrites) {
23167
+ throw new Error(
23168
+ "linear_update_issue needs at least one of title, description, state, assignee, priority, cycle, labels, project, parent, relations, removeRelations"
23169
+ );
22768
23170
  }
22769
- const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
22770
- if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
22771
- throw new Error("Linear issueUpdate failed");
23171
+ let updated;
23172
+ if (hasIssueFields) {
23173
+ const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
23174
+ if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
23175
+ throw new Error("Linear issueUpdate failed");
23176
+ }
23177
+ updated = mapIssue(json.issueUpdate.issue);
23178
+ }
23179
+ if (hasRelationWrites) {
23180
+ const base = updated ?? existing;
23181
+ if (!base) {
23182
+ throw new Error(`Linear issue not found: ${issueId}`);
23183
+ }
23184
+ await applyLinearIssueRelations(base, addRelations, removeRelations, opts);
23185
+ return getLinearIssue(base.id || issueId, opts);
22772
23186
  }
22773
- return mapIssue(json.issueUpdate.issue);
23187
+ return updated;
22774
23188
  }
22775
23189
  async function commentLinearIssue(input, opts) {
22776
23190
  const issueId = input.id.trim();
@@ -24038,14 +24452,23 @@ function registerAbleTimeTools(server) {
24038
24452
  }
24039
24453
  }
24040
24454
  );
24455
+ const relationInputSchema = import_zod4.z.object({
24456
+ type: import_zod4.z.string().describe("blockedBy (this waits on issue) or blocks (issue waits on this)."),
24457
+ issue: import_zod4.z.string().describe("Related task id or reference (CRM-232).")
24458
+ });
24041
24459
  server.tool(
24042
24460
  "abletime_update_task",
24043
- "Update an AbleTime task (id or CRM-232). Pass title, description, and/or state. When reviewing a PR or ticket, wait until they type a next step before changing the task \u2014 do not ask_user after the review. The author is notified.",
24461
+ 'Update an AbleTime task (id or CRM-232). Pass title, description, state, labels, project, parent, and/or relations. labels replaces tags. project is name or id (tasks stay in a project). parent is CRM-232 or "none". relations sets the wait-on dependency (blockedBy/blocks); removeRelations clears it. When reviewing a PR or ticket, wait until they type a next step before changing the task \u2014 do not ask_user after the review. The author is notified.',
24044
24462
  {
24045
24463
  id: import_zod4.z.string(),
24046
24464
  title: import_zod4.z.string().optional(),
24047
24465
  description: import_zod4.z.string().optional(),
24048
- state: import_zod4.z.string().optional()
24466
+ state: import_zod4.z.string().optional(),
24467
+ labels: import_zod4.z.array(import_zod4.z.string()).optional().describe("Replace tags/labels. Empty array clears when the host accepts it."),
24468
+ project: import_zod4.z.string().nullable().optional().describe("Project name or id. AbleTime tasks stay in a project."),
24469
+ parent: import_zod4.z.string().nullable().optional().describe('Parent task id or reference (CRM-232), or "none" to unset.'),
24470
+ relations: import_zod4.z.array(relationInputSchema).optional().describe("Set a wait-on dependency: blockedBy or blocks."),
24471
+ removeRelations: import_zod4.z.array(relationInputSchema).optional().describe("Clear a wait-on dependency.")
24049
24472
  },
24050
24473
  async (args) => {
24051
24474
  try {
@@ -24180,14 +24603,23 @@ function registerGithubIssueTools(server) {
24180
24603
  }
24181
24604
  }
24182
24605
  );
24606
+ const relationInputSchema = import_zod5.z.object({
24607
+ type: import_zod5.z.string().describe("blocks or blockedBy (this issue's view)."),
24608
+ issue: import_zod5.z.string().describe("Related issue #123 or URL.")
24609
+ });
24183
24610
  server.tool(
24184
24611
  "github_update_issue",
24185
- "Update a GitHub issue (#123). Pass title, body, and/or state (open|closed). When reviewing a PR or ticket, wait until they type a next step before changing the issue \u2014 do not ask_user after the review. The author is notified.",
24612
+ 'Update a GitHub issue (#123). Pass title, body, state (open|closed), labels, project, parent, and/or relations. labels replaces all names (pass [] to clear). project is a Project title, or "none" to remove. parent is #123 or "none". relations adds blockedBy/blocks; removeRelations drops them (`gh` 2.94+). When reviewing a PR or ticket, wait until they type a next step before changing the issue \u2014 do not ask_user after the review. The author is notified.',
24186
24613
  {
24187
24614
  id: import_zod5.z.string(),
24188
24615
  title: import_zod5.z.string().optional(),
24189
24616
  body: import_zod5.z.string().optional(),
24190
24617
  state: import_zod5.z.string().optional().describe("open or closed"),
24618
+ labels: import_zod5.z.array(import_zod5.z.string()).optional().describe("Replace all labels. Empty array clears. Call github_get_issue first to keep existing."),
24619
+ project: import_zod5.z.string().nullable().optional().describe('GitHub Project title, or "none" to remove current projects.'),
24620
+ parent: import_zod5.z.string().nullable().optional().describe('Parent issue #123 or URL, or "none" to unset.'),
24621
+ relations: import_zod5.z.array(relationInputSchema).optional().describe("Add blockedBy or blocks relations (issue #123 or URL)."),
24622
+ removeRelations: import_zod5.z.array(relationInputSchema).optional().describe("Remove blockedBy or blocks relations."),
24191
24623
  repoPath: repoPathSchema
24192
24624
  },
24193
24625
  async (args) => {
@@ -24317,9 +24749,13 @@ function registerLinearTools(server) {
24317
24749
  }
24318
24750
  }
24319
24751
  );
24752
+ const relationInputSchema = import_zod6.z.object({
24753
+ type: import_zod6.z.string().describe("blocks, blockedBy, related, duplicate, or duplicateOf (this issue's view)."),
24754
+ issue: import_zod6.z.string().describe("Related issue uuid or identifier (ENG-123).")
24755
+ });
24320
24756
  server.tool(
24321
24757
  "linear_update_issue",
24322
- 'Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, priority, and/or cycle. Cycle is name, number, "current"/"active", or "none" to unschedule. When reviewing a PR or ticket, wait until they type a next step before changing the ticket \u2014 do not ask_user after the review. The author is notified.',
24758
+ 'Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, priority, cycle, labels, project, parent, and/or relations. Cycle/project/parent: name or id, or "none" to clear. labels replaces all names (pass [] to clear; get the issue first to keep existing). relations adds; removeRelations removes by type + issue. When reviewing a PR or ticket, wait until they type a next step before changing the ticket \u2014 do not ask_user after the review. The author is notified.',
24323
24759
  {
24324
24760
  id: import_zod6.z.string(),
24325
24761
  title: import_zod6.z.string().optional(),
@@ -24327,7 +24763,12 @@ function registerLinearTools(server) {
24327
24763
  state: import_zod6.z.string().optional(),
24328
24764
  assignee: import_zod6.z.string().nullable().optional(),
24329
24765
  priority: prioritySchema,
24330
- cycle: import_zod6.z.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.')
24766
+ cycle: import_zod6.z.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.'),
24767
+ labels: import_zod6.z.array(import_zod6.z.string()).optional().describe("Replace all labels (names or ids). Empty array clears. Call linear_get_issue first to keep existing."),
24768
+ project: import_zod6.z.string().nullable().optional().describe('Project name, id, slug, or "none" to unassign.'),
24769
+ parent: import_zod6.z.string().nullable().optional().describe('Parent issue uuid or identifier (ENG-123), or "none" to unset.'),
24770
+ relations: import_zod6.z.array(relationInputSchema).optional().describe("Add relations from this issue's view (blocks, blockedBy, related, duplicate, duplicateOf)."),
24771
+ removeRelations: import_zod6.z.array(relationInputSchema).optional().describe("Remove relations by type + related issue (uuid or ENG-123).")
24331
24772
  },
24332
24773
  async (args) => {
24333
24774
  try {