@selfagency/beans-mcp 0.5.0 → 0.6.1

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.
@@ -22905,6 +22905,26 @@ Output: ${stdout.slice(0, 1e3)}`
22905
22905
  });
22906
22906
  return { initialized: true };
22907
22907
  }
22908
+ async archive() {
22909
+ const { stdout } = await execFileAsync(this.cliPath, ["archive", "--json"], {
22910
+ cwd: this.workspaceRoot,
22911
+ env: this.getSafeEnv(),
22912
+ maxBuffer: 10 * 1024 * 1024,
22913
+ timeout: 3e4
22914
+ });
22915
+ this.invalidateCache();
22916
+ if (!stdout.trim()) {
22917
+ return { archived: true };
22918
+ }
22919
+ try {
22920
+ return JSON.parse(stdout);
22921
+ } catch {
22922
+ return { archived: true, output: stdout.trim() };
22923
+ }
22924
+ }
22925
+ async queryGraphql(query, variables) {
22926
+ return this.executeGraphQL(query, variables);
22927
+ }
22908
22928
  async list(options) {
22909
22929
  const filter = {};
22910
22930
  if (options?.status && options.status.length > 0) {
@@ -23059,32 +23079,39 @@ Output: ${stdout.slice(0, 1e3)}`
23059
23079
  return { deleted: true, beanId };
23060
23080
  }
23061
23081
  async bulkCreate(beans, defaultParent) {
23062
- const results = [];
23063
- for (const item of beans) {
23064
- try {
23065
- const bean = await this.create({
23082
+ const settled = await Promise.allSettled(
23083
+ beans.map(
23084
+ async (item) => this.create({
23066
23085
  ...item,
23067
23086
  parent: item.parent ?? defaultParent
23068
- });
23069
- results.push({ bean });
23070
- } catch (error48) {
23071
- results.push({ error: error48.message });
23072
- }
23073
- }
23074
- return results;
23087
+ })
23088
+ )
23089
+ );
23090
+ return settled.map(
23091
+ (result) => result.status === "fulfilled" ? { bean: result.value } : { error: result.reason instanceof Error ? result.reason.message : String(result.reason) }
23092
+ );
23075
23093
  }
23076
23094
  async bulkUpdate(beans, defaultParent) {
23077
- const results = [];
23078
- for (const { beanId, ...updates } of beans) {
23079
- try {
23095
+ const settled = await Promise.allSettled(
23096
+ beans.map(async ({ beanId, ...updates }) => {
23080
23097
  const resolvedParent = updates.parent ?? (updates.clearParent ? void 0 : defaultParent);
23081
23098
  const bean = await this.update(beanId, { ...updates, parent: resolvedParent });
23082
- results.push({ beanId, bean });
23083
- } catch (error48) {
23084
- results.push({ beanId, error: error48.message });
23099
+ return { beanId, bean };
23100
+ })
23101
+ );
23102
+ return settled.map((result, index) => {
23103
+ const beanId = beans[index]?.beanId;
23104
+ if (!beanId) {
23105
+ return { beanId: "unknown", error: "Unknown bean id" };
23085
23106
  }
23086
- }
23087
- return results;
23107
+ if (result.status === "fulfilled") {
23108
+ return result.value;
23109
+ }
23110
+ return {
23111
+ beanId,
23112
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
23113
+ };
23114
+ });
23088
23115
  }
23089
23116
  async openConfig() {
23090
23117
  const configPath = (0, import_node_path2.join)(this.workspaceRoot, ".beans.yml");
@@ -23119,9 +23146,12 @@ Output: ${stdout.slice(0, 1e3)}`
23119
23146
  const outputPath = (0, import_node_path2.resolve)(
23120
23147
  process.env.BEANS_VSCODE_OUTPUT_LOG || (0, import_node_path2.join)(this.workspaceRoot, ".vscode", "logs", "beans-output.log")
23121
23148
  );
23122
- const isWithinWorkspace = isPathWithinRoot(this.workspaceRoot, outputPath);
23149
+ const canonicalOutputPath = await (0, import_promises.realpath)(outputPath).catch(() => outputPath);
23150
+ const canonicalWorkspaceRoot = await (0, import_promises.realpath)(this.workspaceRoot).catch(() => (0, import_node_path2.resolve)(this.workspaceRoot));
23151
+ const isWithinWorkspace = isPathWithinRoot(canonicalWorkspaceRoot, canonicalOutputPath);
23123
23152
  const vscodeLogDir = process.env.BEANS_VSCODE_LOG_DIR || this.logDir ? (0, import_node_path2.resolve)(process.env.BEANS_VSCODE_LOG_DIR || this.logDir || "") : void 0;
23124
- const isWithinVscodeLogDir = vscodeLogDir ? isPathWithinRoot(vscodeLogDir, outputPath) : false;
23153
+ const canonicalVscodeLogDir = vscodeLogDir ? await (0, import_promises.realpath)(vscodeLogDir).catch(() => (0, import_node_path2.resolve)(vscodeLogDir)) : void 0;
23154
+ const isWithinVscodeLogDir = canonicalVscodeLogDir ? isPathWithinRoot(canonicalVscodeLogDir, canonicalOutputPath) : false;
23125
23155
  if (!isWithinWorkspace && !isWithinVscodeLogDir) {
23126
23156
  throw new Error("Output log path must stay within the workspace or VS Code log directory");
23127
23157
  }
@@ -23200,6 +23230,105 @@ Output: ${stdout.slice(0, 1e3)}`
23200
23230
  escapeForYamlDoubleQuoted(value) {
23201
23231
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
23202
23232
  }
23233
+ shouldQuoteFrontmatterValue(value) {
23234
+ return !/^[A-Za-z0-9._-]+$/.test(value);
23235
+ }
23236
+ parseFrontmatterLine(line) {
23237
+ const separatorIndex = line.indexOf(":");
23238
+ if (separatorIndex <= 0) {
23239
+ return null;
23240
+ }
23241
+ const key = line.slice(0, separatorIndex).trim();
23242
+ if (key.length === 0) {
23243
+ return null;
23244
+ }
23245
+ for (const character of key) {
23246
+ const isAlphaNumericUnderscore = character >= "a" && character <= "z" || character >= "A" && character <= "Z" || character >= "0" && character <= "9" || character === "_";
23247
+ if (!isAlphaNumericUnderscore) {
23248
+ return null;
23249
+ }
23250
+ }
23251
+ const rawValue = line.slice(separatorIndex + 1).trimStart();
23252
+ return { key, rawValue };
23253
+ }
23254
+ buildFrontmatterIndex(frontmatterLines) {
23255
+ const indexByKey = /* @__PURE__ */ new Map();
23256
+ frontmatterLines.forEach((line, index) => {
23257
+ const parsed = this.parseFrontmatterLine(line);
23258
+ if (!parsed) {
23259
+ return;
23260
+ }
23261
+ indexByKey.set(parsed.key, index);
23262
+ });
23263
+ return indexByKey;
23264
+ }
23265
+ serializeFrontmatterValue(key, value) {
23266
+ if (Array.isArray(value)) {
23267
+ return JSON.stringify(value);
23268
+ }
23269
+ if (key === "title") {
23270
+ return this.normalizeFrontmatterTitleValue(value);
23271
+ }
23272
+ if (this.shouldQuoteFrontmatterValue(value)) {
23273
+ return `"${this.escapeForYamlDoubleQuoted(value)}"`;
23274
+ }
23275
+ return value;
23276
+ }
23277
+ deserializeFrontmatterValue(value) {
23278
+ const trimmed = value.trim();
23279
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
23280
+ try {
23281
+ const parsed = JSON.parse(trimmed);
23282
+ if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
23283
+ return parsed;
23284
+ }
23285
+ } catch {
23286
+ }
23287
+ }
23288
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
23289
+ return trimmed.slice(1, -1).replaceAll('\\"', '"').replaceAll("\\\\", "\\").replaceAll("''", "'");
23290
+ }
23291
+ return trimmed;
23292
+ }
23293
+ splitFrontmatterDocument(content) {
23294
+ const crlfOpen = content.startsWith("---\r\n");
23295
+ const lfOpen = content.startsWith("---\n");
23296
+ const eol = crlfOpen ? "\r\n" : "\n";
23297
+ if (!crlfOpen && !lfOpen) {
23298
+ return { eol: "\n", hasFrontmatter: false, frontmatterLines: [], body: content };
23299
+ }
23300
+ const openEnd = `---${eol}`.length;
23301
+ const closeMarker = `${eol}---`;
23302
+ const closeIdx = content.indexOf(closeMarker, openEnd);
23303
+ if (closeIdx === -1) {
23304
+ return { eol, hasFrontmatter: false, frontmatterLines: [], body: content };
23305
+ }
23306
+ const frontmatter = content.slice(openEnd, closeIdx);
23307
+ const body = content.slice(closeIdx + closeMarker.length);
23308
+ return {
23309
+ eol,
23310
+ hasFrontmatter: true,
23311
+ frontmatterLines: frontmatter.length > 0 ? frontmatter.split(eol) : [],
23312
+ body
23313
+ };
23314
+ }
23315
+ parseFrontmatterFields(frontmatterLines) {
23316
+ const fields = {};
23317
+ for (const line of frontmatterLines) {
23318
+ const parsed = this.parseFrontmatterLine(line);
23319
+ if (!parsed) {
23320
+ continue;
23321
+ }
23322
+ const { valuePart } = this.splitYamlInlineComment(parsed.rawValue);
23323
+ fields[parsed.key] = this.deserializeFrontmatterValue(valuePart);
23324
+ }
23325
+ return fields;
23326
+ }
23327
+ async writeFileAtomically(absolutePath, content) {
23328
+ const tempPath = `${absolutePath}.tmp-${process.pid}-${Date.now()}`;
23329
+ await (0, import_promises.writeFile)(tempPath, content, "utf8");
23330
+ await (0, import_promises.rename)(tempPath, absolutePath);
23331
+ }
23203
23332
  /**
23204
23333
  * Normalise a raw YAML title value to a double-quoted scalar.
23205
23334
  * Handles: empty, already double-quoted, single-quoted (unescaping `''`),
@@ -23270,14 +23399,66 @@ Output: ${stdout.slice(0, 1e3)}`
23270
23399
  await (0, import_promises.writeFile)(absolutePath, fixed, "utf8");
23271
23400
  return { path: absolutePath, bytes: Buffer.byteLength(fixed, "utf8") };
23272
23401
  }
23402
+ async updateBeanFrontmatter(relativePath, updates) {
23403
+ const absolutePath = this.resolveBeanFilePath(relativePath);
23404
+ const content = await (0, import_promises.readFile)(absolutePath, "utf8");
23405
+ const { eol, hasFrontmatter, frontmatterLines, body } = this.splitFrontmatterDocument(content);
23406
+ const updatedFields = Object.entries(updates).filter(([, value]) => value !== void 0).map(([key]) => key);
23407
+ if (updatedFields.length === 0) {
23408
+ throw new Error("At least one frontmatter field update is required");
23409
+ }
23410
+ const nextLines = [...frontmatterLines];
23411
+ let indexByKey = this.buildFrontmatterIndex(nextLines);
23412
+ for (const [key, value] of Object.entries(updates)) {
23413
+ if (value === void 0) {
23414
+ continue;
23415
+ }
23416
+ const existingIndex = indexByKey.get(key);
23417
+ if (value === null) {
23418
+ if (existingIndex !== void 0) {
23419
+ nextLines.splice(existingIndex, 1);
23420
+ indexByKey = this.buildFrontmatterIndex(nextLines);
23421
+ }
23422
+ continue;
23423
+ }
23424
+ const serialized = `${key}: ${this.serializeFrontmatterValue(key, value)}`;
23425
+ if (existingIndex !== void 0) {
23426
+ const existingLine = nextLines[existingIndex] ?? "";
23427
+ const existingParsed = this.parseFrontmatterLine(existingLine);
23428
+ const commentPart = existingParsed ? this.splitYamlInlineComment(existingParsed.rawValue).commentPart : "";
23429
+ nextLines[existingIndex] = `${serialized}${commentPart}`;
23430
+ } else {
23431
+ nextLines.push(serialized);
23432
+ indexByKey.set(key, nextLines.length - 1);
23433
+ }
23434
+ }
23435
+ const frontmatterBlock = nextLines.length > 0 ? nextLines.join(eol) : "";
23436
+ const nextContent = hasFrontmatter ? `---${eol}${frontmatterBlock}${eol}---${body}` : `---${eol}${frontmatterBlock}${eol}---${eol}${body}`;
23437
+ const fixed = this.quoteFrontmatterTitles(nextContent);
23438
+ await this.writeFileAtomically(absolutePath, fixed);
23439
+ return {
23440
+ path: absolutePath,
23441
+ bytes: Buffer.byteLength(fixed, "utf8"),
23442
+ updatedFields,
23443
+ frontmatter: this.parseFrontmatterFields(this.splitFrontmatterDocument(fixed).frontmatterLines)
23444
+ };
23445
+ }
23273
23446
  async createBeanFile(relativePath, content, options) {
23274
23447
  const absolutePath = this.resolveBeanFilePath(relativePath);
23275
23448
  const fixed = this.quoteFrontmatterTitles(content);
23276
23449
  await (0, import_promises.mkdir)((0, import_node_path2.dirname)(absolutePath), { recursive: true });
23277
- await (0, import_promises.writeFile)(absolutePath, fixed, {
23278
- encoding: "utf8",
23279
- flag: options?.overwrite ? "w" : "wx"
23280
- });
23450
+ try {
23451
+ await (0, import_promises.writeFile)(absolutePath, fixed, {
23452
+ encoding: "utf8",
23453
+ flag: options?.overwrite ? "w" : "wx"
23454
+ });
23455
+ } catch (error48) {
23456
+ const maybeNodeError = error48;
23457
+ if (maybeNodeError.code === "EEXIST" && !options?.overwrite) {
23458
+ throw new Error("Bean file already exists. Pass overwrite=true to replace it.");
23459
+ }
23460
+ throw error48;
23461
+ }
23281
23462
  return {
23282
23463
  path: absolutePath,
23283
23464
  bytes: Buffer.byteLength(fixed, "utf8"),
@@ -31368,14 +31549,16 @@ var import_node_util2 = require("util");
31368
31549
  // package.json
31369
31550
  var package_default = {
31370
31551
  name: "@selfagency/beans-mcp",
31371
- version: "0.5.0",
31552
+ version: "0.6.1",
31372
31553
  private: false,
31373
31554
  description: "MCP (Model Context Protocol) server for Beans issue tracker",
31374
- author: {
31375
- name: "Daniel Sieradski",
31376
- email: "daniel@self.agency",
31377
- url: "https://self.agency"
31378
- },
31555
+ keywords: [
31556
+ "ai",
31557
+ "beans",
31558
+ "issue-tracker",
31559
+ "mcp",
31560
+ "model-context-protocol"
31561
+ ],
31379
31562
  homepage: "https://github.com/selfagency/beans-mcp",
31380
31563
  bugs: {
31381
31564
  url: "https://github.com/selfagency/beans-mcp/issues"
@@ -31384,15 +31567,12 @@ var package_default = {
31384
31567
  type: "git",
31385
31568
  url: "git+https://github.com/selfagency/beans-mcp.git"
31386
31569
  },
31387
- mcpName: "io.github.selfagency/beans-mcp",
31388
- keywords: [
31389
- "beans",
31390
- "mcp",
31391
- "model-context-protocol",
31392
- "issue-tracker",
31393
- "ai"
31394
- ],
31395
31570
  license: "MIT",
31571
+ author: {
31572
+ name: "Daniel Sieradski",
31573
+ email: "daniel@self.agency",
31574
+ url: "https://self.agency"
31575
+ },
31396
31576
  type: "module",
31397
31577
  exports: {
31398
31578
  ".": {
@@ -31407,46 +31587,57 @@ var package_default = {
31407
31587
  bin: {
31408
31588
  "beans-mcp": "dist/beans-mcp-server.cjs"
31409
31589
  },
31590
+ files: [
31591
+ "dist",
31592
+ "skills",
31593
+ "README.md",
31594
+ "LICENSE.txt"
31595
+ ],
31410
31596
  scripts: {
31411
31597
  build: "tsup",
31598
+ postbuild: "node ./scripts/write-dist-package.js",
31599
+ "docs:build": "vitepress build docs",
31600
+ "docs:dev": "vitepress dev docs",
31601
+ "docs:preview": "vitepress preview docs",
31412
31602
  format: "oxfmt",
31413
- "lint:fix": "oxlint --fix",
31414
31603
  lint: "oxlint",
31415
- postbuild: "node ./scripts/write-dist-package.js",
31604
+ "lint:fix": "oxlint --fix",
31416
31605
  prepare: "husky",
31417
31606
  release: "zx ./scripts/release.js",
31607
+ test: "vitest run",
31418
31608
  "test:coverage": "vitest run --coverage",
31419
31609
  "test:watch": "vitest",
31420
- test: "vitest run",
31421
31610
  "type-check": "tsc --noEmit"
31422
31611
  },
31612
+ "lint-staged": {
31613
+ "src/**/*.ts": [
31614
+ "pnpm run lint:fix",
31615
+ "pnpm run format"
31616
+ ]
31617
+ },
31423
31618
  devDependencies: {
31424
31619
  "@modelcontextprotocol/sdk": "^1.29.0",
31425
31620
  "@octokit/rest": "^22.0.1",
31426
- "@types/node": "25.5.2",
31427
- "@vitest/coverage-v8": "^4.1.2",
31428
- "@vitest/ui": "4.1.2",
31621
+ "@types/node": "25.6.0",
31622
+ "@vitest/coverage-v8": "^4.1.4",
31623
+ "@vitest/ui": "4.1.4",
31429
31624
  husky: "^9.1.7",
31430
31625
  "lint-staged": "^16.4.0",
31431
31626
  ora: "^9.3.0",
31432
- oxfmt: "^0.43.0",
31433
- oxlint: "^1.58.0",
31434
- "oxlint-tsgolint": "^0.20.0",
31627
+ oxfmt: "^0.45.0",
31628
+ oxlint: "^1.60.0",
31629
+ "oxlint-tsgolint": "^0.21.1",
31435
31630
  tsup: "8.5.1",
31436
- typescript: "6.0.2",
31437
- vitest: "4.1.2",
31631
+ typescript: "6.0.3",
31632
+ vitepress: "^1.6.4",
31633
+ vitest: "4.1.4",
31438
31634
  zod: "4.3.6",
31439
31635
  zx: "^8.8.5"
31440
31636
  },
31441
31637
  engines: {
31442
31638
  node: ">=18"
31443
31639
  },
31444
- "lint-staged": {
31445
- "src/**/*.ts": [
31446
- "pnpm run lint:fix",
31447
- "pnpm run format"
31448
- ]
31449
- }
31640
+ mcpName: "io.github.selfagency/beans-mcp"
31450
31641
  };
31451
31642
 
31452
31643
  // src/internal/queryHelpers.ts
@@ -31611,6 +31802,8 @@ var MAX_PATH_LENGTH = 1024;
31611
31802
  init_utils();
31612
31803
  var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
31613
31804
  var PACKAGE_VERSION = package_default.version ?? "0.0.0-dev";
31805
+ var CLOSED_STATUSES = /* @__PURE__ */ new Set(["completed", "scrapped"]);
31806
+ var BEAN_ID_HINT = "Missing required field `beanId`. Did you mean `beanId`?";
31614
31807
  function getSafeCliEnv(env) {
31615
31808
  const whitelist = ["PATH", "HOME", "USER", "LANG", "LC_ALL", "LC_CTYPE", "SHELL"];
31616
31809
  const safeEnv = {};
@@ -31631,7 +31824,8 @@ function extractVersionFromOutput(output) {
31631
31824
  if (!trimmed) {
31632
31825
  return null;
31633
31826
  }
31634
- const match = trimmed.match(/(?:^|[^\d])v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/);
31827
+ const versionRegex = /(?:^|[^\d])v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/;
31828
+ const match = versionRegex.exec(trimmed);
31635
31829
  return match?.[1] ?? null;
31636
31830
  }
31637
31831
  async function detectBeansCliVersion(cliPath, workspaceRoot) {
@@ -31648,10 +31842,10 @@ ${stderr}`);
31648
31842
  return null;
31649
31843
  }
31650
31844
  }
31651
- async function getBeanById(backend, beanId) {
31845
+ async function getBeanById(backend, beanId, beans) {
31652
31846
  try {
31653
- const beans = await backend.list();
31654
- const found = beans.find((b) => b.id === beanId);
31847
+ const allBeans = beans ?? await backend.list();
31848
+ const found = allBeans.find((b) => b.id === beanId);
31655
31849
  if (!found) {
31656
31850
  throw new Error(`Bean not found: ${beanId}`);
31657
31851
  }
@@ -31660,12 +31854,111 @@ async function getBeanById(backend, beanId) {
31660
31854
  throw new Error(`Failed to fetch bean ${beanId}: ${error48.message}`);
31661
31855
  }
31662
31856
  }
31857
+ function collectDescendantBeans(beans, rootBeanId) {
31858
+ const byParent = /* @__PURE__ */ new Map();
31859
+ const byId = new Map(beans.map((bean) => [bean.id, bean]));
31860
+ for (const bean of beans) {
31861
+ if (!bean.parentId) {
31862
+ continue;
31863
+ }
31864
+ const children = byParent.get(bean.parentId) ?? [];
31865
+ children.push(bean.id);
31866
+ byParent.set(bean.parentId, children);
31867
+ }
31868
+ const queue = [...byParent.get(rootBeanId) ?? []];
31869
+ const visited = /* @__PURE__ */ new Set();
31870
+ const descendants = [];
31871
+ while (queue.length > 0) {
31872
+ const currentId = queue.shift();
31873
+ if (!currentId || visited.has(currentId)) {
31874
+ continue;
31875
+ }
31876
+ visited.add(currentId);
31877
+ const currentBean = byId.get(currentId);
31878
+ if (!currentBean) {
31879
+ continue;
31880
+ }
31881
+ descendants.push(currentBean);
31882
+ const children = byParent.get(currentId);
31883
+ if (children && children.length > 0) {
31884
+ queue.push(...children);
31885
+ }
31886
+ }
31887
+ return descendants;
31888
+ }
31889
+ async function cascadeStatusToDescendants(backend, rootBeanId, targetStatus, options) {
31890
+ const beans = options?.beans ?? await backend.list();
31891
+ const descendants = collectDescendantBeans(beans, rootBeanId);
31892
+ const updatedBeanIds = [];
31893
+ const skippedBeanIds = [];
31894
+ const errors = [];
31895
+ const toUpdate = [];
31896
+ for (const bean of descendants) {
31897
+ if (options?.onlyCurrentStatuses && !options.onlyCurrentStatuses.has(bean.status)) {
31898
+ skippedBeanIds.push(bean.id);
31899
+ continue;
31900
+ }
31901
+ toUpdate.push(bean);
31902
+ }
31903
+ const settled = await Promise.allSettled(
31904
+ toUpdate.map(async (bean) => backend.update(bean.id, { status: targetStatus }))
31905
+ );
31906
+ settled.forEach((result, index) => {
31907
+ const bean = toUpdate[index];
31908
+ if (!bean) {
31909
+ return;
31910
+ }
31911
+ if (result.status === "fulfilled") {
31912
+ updatedBeanIds.push(bean.id);
31913
+ return;
31914
+ }
31915
+ errors.push({
31916
+ beanId: bean.id,
31917
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
31918
+ });
31919
+ });
31920
+ return {
31921
+ totalDescendants: descendants.length,
31922
+ updatedBeanIds,
31923
+ skippedBeanIds,
31924
+ errors
31925
+ };
31926
+ }
31927
+ function completeMarkdownTasks(body) {
31928
+ const lines = body.split(/\r?\n/);
31929
+ let totalTaskCount = 0;
31930
+ let updatedTaskCount = 0;
31931
+ const taskLinePattern = /^\s*(?:[-*+]|\d+\.)\s+\[[ xX]\]/;
31932
+ const uncheckedTaskLinePattern = /^(\s*(?:[-*+]|\d+\.)\s+\[)\s(\].*)$/;
31933
+ const nextLines = lines.map((line) => {
31934
+ if (!taskLinePattern.test(line)) {
31935
+ return line;
31936
+ }
31937
+ totalTaskCount += 1;
31938
+ const uncheckedMatch = uncheckedTaskLinePattern.exec(line);
31939
+ if (!uncheckedMatch) {
31940
+ return line;
31941
+ }
31942
+ updatedTaskCount += 1;
31943
+ return `${uncheckedMatch[1]}x${uncheckedMatch[2]}`;
31944
+ });
31945
+ const nextBody = nextLines.join("\n");
31946
+ return { nextBody, totalTaskCount, updatedTaskCount };
31947
+ }
31663
31948
  function initHandler(backend) {
31664
31949
  return async ({ prefix }) => {
31665
31950
  const result = await backend.init(prefix);
31666
31951
  return makeTextAndStructured(result);
31667
31952
  };
31668
31953
  }
31954
+ function archiveHandler(backend) {
31955
+ return async () => {
31956
+ if (typeof backend.archive !== "function") {
31957
+ throw new TypeError("Archive is not supported by the current backend");
31958
+ }
31959
+ return makeTextAndStructured(await backend.archive());
31960
+ };
31961
+ }
31669
31962
  function viewHandler(backend) {
31670
31963
  return async ({ beanId, beanIds }) => {
31671
31964
  const ids = Array.isArray(beanIds) && beanIds.length > 0 ? beanIds : beanId ? [beanId] : [];
@@ -31703,7 +31996,14 @@ async function checkVersionCompatibility(cliPath, workspaceRoot, detector) {
31703
31996
  }
31704
31997
  }
31705
31998
  function createHandler(backend) {
31706
- return async (input) => makeTextAndStructured({ bean: await backend.create(input) });
31999
+ return async (input) => {
32000
+ const bean = await backend.create(input);
32001
+ const warnings = input.description !== void 0 ? ["`description` is deprecated; use `body` instead."] : void 0;
32002
+ return makeTextAndStructured({
32003
+ bean,
32004
+ ...warnings ? { warnings } : {}
32005
+ });
32006
+ };
31707
32007
  }
31708
32008
  function editHandler(backend) {
31709
32009
  return async ({
@@ -31717,18 +32017,30 @@ function reopenHandler(backend) {
31717
32017
  requiredCurrentStatus,
31718
32018
  targetStatus
31719
32019
  }) => {
31720
- const bean = await getBeanById(backend, beanId);
31721
- if (bean.status !== requiredCurrentStatus) {
31722
- throw new Error(`Bean ${beanId} is not ${requiredCurrentStatus}`);
32020
+ const beans = await backend.list();
32021
+ const bean = await getBeanById(backend, beanId, beans);
32022
+ if (bean.status === requiredCurrentStatus) {
32023
+ const updatedParentBean = await backend.update(beanId, { status: targetStatus });
32024
+ const cascade = await cascadeStatusToDescendants(backend, beanId, targetStatus, {
32025
+ onlyCurrentStatuses: CLOSED_STATUSES,
32026
+ beans
32027
+ });
32028
+ return makeTextAndStructured({
32029
+ bean: updatedParentBean,
32030
+ cascade: {
32031
+ totalDescendants: cascade.totalDescendants,
32032
+ updatedBeanIds: cascade.updatedBeanIds,
32033
+ skippedBeanIds: cascade.skippedBeanIds,
32034
+ errors: cascade.errors
32035
+ }
32036
+ });
31723
32037
  }
31724
- return makeTextAndStructured({
31725
- bean: await backend.update(beanId, { status: targetStatus })
31726
- });
32038
+ throw new Error(`Bean ${beanId} is not ${requiredCurrentStatus}`);
31727
32039
  };
31728
32040
  }
31729
32041
  function updateHandler(backend) {
31730
- return async (input) => makeTextAndStructured({
31731
- bean: await backend.update(input.beanId, {
32042
+ return async (input) => {
32043
+ const updatedBean = await backend.update(input.beanId, {
31732
32044
  status: input.status,
31733
32045
  type: input.type,
31734
32046
  priority: input.priority,
@@ -31740,8 +32052,37 @@ function updateHandler(backend) {
31740
32052
  bodyAppend: input.bodyAppend,
31741
32053
  bodyReplace: input.bodyReplace,
31742
32054
  ifMatch: input.ifMatch
31743
- })
31744
- });
32055
+ });
32056
+ const closeStatus = input.status;
32057
+ const shouldCascadeClose = Boolean(closeStatus && CLOSED_STATUSES.has(closeStatus));
32058
+ const cascade = shouldCascadeClose ? await cascadeStatusToDescendants(backend, input.beanId, closeStatus, {
32059
+ beans: await backend.list()
32060
+ }) : null;
32061
+ return makeTextAndStructured({
32062
+ bean: updatedBean,
32063
+ ...cascade ? {
32064
+ cascade: {
32065
+ totalDescendants: cascade.totalDescendants,
32066
+ updatedBeanIds: cascade.updatedBeanIds,
32067
+ skippedBeanIds: cascade.skippedBeanIds,
32068
+ errors: cascade.errors
32069
+ }
32070
+ } : {}
32071
+ });
32072
+ };
32073
+ }
32074
+ function completeTasksHandler(backend) {
32075
+ return async ({ beanId }) => {
32076
+ const bean = await getBeanById(backend, beanId);
32077
+ const { nextBody, totalTaskCount, updatedTaskCount } = completeMarkdownTasks(bean.body || "");
32078
+ const updatedBean = updatedTaskCount > 0 ? await backend.update(beanId, { body: nextBody }) : bean;
32079
+ return makeTextAndStructured({
32080
+ bean: updatedBean,
32081
+ totalTaskCount,
32082
+ updatedTaskCount,
32083
+ unchangedTaskCount: totalTaskCount - updatedTaskCount
32084
+ });
32085
+ };
31745
32086
  }
31746
32087
  function deleteHandler(backend) {
31747
32088
  return async ({ beanId, beanIds, force }) => {
@@ -31795,11 +32136,17 @@ function deleteHandler(backend) {
31795
32136
  function bulkCreateHandler(backend) {
31796
32137
  return async (input) => {
31797
32138
  const results = await backend.bulkCreate(input.beans, input.parent);
32139
+ const deprecatedDescriptionCount = input.beans.filter((bean) => bean.description !== void 0).length;
31798
32140
  return makeTextAndStructured({
31799
32141
  results,
31800
32142
  requestedCount: input.beans.length,
31801
32143
  successCount: results.filter((r) => r.bean).length,
31802
- failedCount: results.filter((r) => r.error).length
32144
+ failedCount: results.filter((r) => r.error).length,
32145
+ ...deprecatedDescriptionCount > 0 ? {
32146
+ warnings: [
32147
+ `Found ${deprecatedDescriptionCount} bean(s) using deprecated field \`description\`; use \`body\` instead.`
32148
+ ]
32149
+ } : {}
31803
32150
  });
31804
32151
  };
31805
32152
  }
@@ -31815,14 +32162,24 @@ function bulkUpdateHandler(backend) {
31815
32162
  };
31816
32163
  }
31817
32164
  function queryHandler(backend) {
31818
- return async (opts) => handleQueryOperation(backend, opts);
32165
+ return async (opts) => {
32166
+ if (opts.operation === "graphql") {
32167
+ if (typeof backend.queryGraphql !== "function") {
32168
+ throw new TypeError("GraphQL passthrough is not supported by the current backend");
32169
+ }
32170
+ const result = await backend.queryGraphql(opts.graphql || "", opts.variables);
32171
+ return makeTextAndStructured({ data: result.data, errors: result.errors ?? [] });
32172
+ }
32173
+ return handleQueryOperation(backend, opts);
32174
+ };
31819
32175
  }
31820
32176
  function beanFileHandler(backend) {
31821
32177
  return async ({
31822
32178
  operation,
31823
32179
  path,
31824
32180
  content,
31825
- overwrite
32181
+ overwrite,
32182
+ fields
31826
32183
  }) => {
31827
32184
  if (operation === "read") {
31828
32185
  return makeTextAndStructured(await backend.readBeanFile(path));
@@ -31833,6 +32190,9 @@ function beanFileHandler(backend) {
31833
32190
  if (operation === "create") {
31834
32191
  return makeTextAndStructured(await backend.createBeanFile(path, content || "", { overwrite }));
31835
32192
  }
32193
+ if (operation === "update_frontmatter") {
32194
+ return makeTextAndStructured(await backend.updateBeanFrontmatter(path, fields || {}));
32195
+ }
31836
32196
  if (operation === "delete") {
31837
32197
  return makeTextAndStructured(await backend.deleteBeanFile(path));
31838
32198
  }
@@ -31867,6 +32227,21 @@ function registerTools(server, backend) {
31867
32227
  },
31868
32228
  initHandler(backend)
31869
32229
  );
32230
+ server.registerTool(
32231
+ "beans_archive",
32232
+ {
32233
+ title: "Archive Beans",
32234
+ description: "Archive completed or scrapped beans, equivalent to the beans CLI archive command.",
32235
+ inputSchema: external_exports3.object({}),
32236
+ annotations: {
32237
+ readOnlyHint: false,
32238
+ destructiveHint: false,
32239
+ idempotentHint: false,
32240
+ openWorldHint: false
32241
+ }
32242
+ },
32243
+ archiveHandler(backend)
32244
+ );
31870
32245
  server.registerTool(
31871
32246
  "beans_view",
31872
32247
  {
@@ -31876,7 +32251,7 @@ function registerTools(server, backend) {
31876
32251
  beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH).optional(),
31877
32252
  beanIds: external_exports3.array(external_exports3.string().min(1).max(MAX_ID_LENGTH)).optional()
31878
32253
  }).refine((input) => Boolean(input.beanId) || Array.isArray(input.beanIds) && input.beanIds.length > 0, {
31879
- message: "Either beanId or beanIds must be provided"
32254
+ message: `Either beanId or beanIds must be provided. ${BEAN_ID_HINT}`
31880
32255
  }),
31881
32256
  annotations: {
31882
32257
  readOnlyHint: true,
@@ -31916,7 +32291,7 @@ function registerTools(server, backend) {
31916
32291
  title: "Edit Bean Metadata",
31917
32292
  description: "Update bean metadata fields (status/type/priority/parent/blocking).",
31918
32293
  inputSchema: external_exports3.object({
31919
- beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH),
32294
+ beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH).optional(),
31920
32295
  status: external_exports3.string().max(MAX_METADATA_LENGTH).optional(),
31921
32296
  type: external_exports3.string().max(MAX_METADATA_LENGTH).optional(),
31922
32297
  priority: external_exports3.string().max(MAX_METADATA_LENGTH).optional(),
@@ -31924,7 +32299,11 @@ function registerTools(server, backend) {
31924
32299
  clearParent: external_exports3.boolean().optional(),
31925
32300
  blocking: external_exports3.array(external_exports3.string().max(MAX_ID_LENGTH)).optional(),
31926
32301
  blockedBy: external_exports3.array(external_exports3.string().max(MAX_ID_LENGTH)).optional()
31927
- }),
32302
+ }).superRefine((input, ctx) => {
32303
+ if (!input.beanId) {
32304
+ ctx.addIssue({ code: external_exports3.ZodIssueCode.custom, path: ["beanId"], message: BEAN_ID_HINT });
32305
+ }
32306
+ }).transform((input) => ({ ...input, beanId: input.beanId })),
31928
32307
  annotations: {
31929
32308
  readOnlyHint: false,
31930
32309
  destructiveHint: false,
@@ -31940,10 +32319,14 @@ function registerTools(server, backend) {
31940
32319
  title: "Reopen Bean",
31941
32320
  description: "Reopen a completed or scrapped bean into a non-closed status.",
31942
32321
  inputSchema: external_exports3.object({
31943
- beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH),
32322
+ beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH).optional(),
31944
32323
  requiredCurrentStatus: external_exports3.enum(["completed", "scrapped"]),
31945
32324
  targetStatus: external_exports3.string().max(MAX_METADATA_LENGTH).default("todo")
31946
- }),
32325
+ }).superRefine((input, ctx) => {
32326
+ if (!input.beanId) {
32327
+ ctx.addIssue({ code: external_exports3.ZodIssueCode.custom, path: ["beanId"], message: BEAN_ID_HINT });
32328
+ }
32329
+ }).transform((input) => ({ ...input, beanId: input.beanId })),
31947
32330
  annotations: {
31948
32331
  readOnlyHint: false,
31949
32332
  destructiveHint: false,
@@ -31959,7 +32342,7 @@ function registerTools(server, backend) {
31959
32342
  title: "Update Bean",
31960
32343
  description: "Update bean metadata fields (status/type/priority/parent/blocking). Consolidated replacement for per-field update tools.",
31961
32344
  inputSchema: external_exports3.object({
31962
- beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH),
32345
+ beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH).optional(),
31963
32346
  status: external_exports3.string().max(MAX_METADATA_LENGTH).optional(),
31964
32347
  type: external_exports3.string().max(MAX_METADATA_LENGTH).optional(),
31965
32348
  priority: external_exports3.string().max(MAX_METADATA_LENGTH).optional(),
@@ -31976,12 +32359,16 @@ function registerTools(server, backend) {
31976
32359
  })
31977
32360
  ).optional(),
31978
32361
  ifMatch: external_exports3.string().max(MAX_METADATA_LENGTH).optional()
32362
+ }).superRefine((input, ctx) => {
32363
+ if (!input.beanId) {
32364
+ ctx.addIssue({ code: external_exports3.ZodIssueCode.custom, path: ["beanId"], message: BEAN_ID_HINT });
32365
+ }
31979
32366
  }).refine(
31980
32367
  (input) => !(input.body !== void 0 && (input.bodyAppend !== void 0 || input.bodyReplace !== void 0)),
31981
32368
  {
31982
32369
  message: "body cannot be combined with bodyAppend/bodyReplace"
31983
32370
  }
31984
- ),
32371
+ ).transform((input) => ({ ...input, beanId: input.beanId })),
31985
32372
  annotations: {
31986
32373
  readOnlyHint: false,
31987
32374
  destructiveHint: false,
@@ -32001,7 +32388,7 @@ function registerTools(server, backend) {
32001
32388
  beanIds: external_exports3.array(external_exports3.string().min(1).max(MAX_ID_LENGTH)).optional(),
32002
32389
  force: external_exports3.boolean().default(false)
32003
32390
  }).refine((input) => Boolean(input.beanId) || Array.isArray(input.beanIds) && input.beanIds.length > 0, {
32004
- message: "Either beanId or beanIds must be provided"
32391
+ message: `Either beanId or beanIds must be provided. ${BEAN_ID_HINT}`
32005
32392
  }),
32006
32393
  annotations: {
32007
32394
  readOnlyHint: false,
@@ -32040,7 +32427,7 @@ function registerTools(server, backend) {
32040
32427
  bulkCreateHandler(backend)
32041
32428
  );
32042
32429
  const beanUpdateItemSchema = external_exports3.object({
32043
- beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH),
32430
+ beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH).optional(),
32044
32431
  status: external_exports3.string().max(MAX_METADATA_LENGTH).optional(),
32045
32432
  type: external_exports3.string().max(MAX_METADATA_LENGTH).optional(),
32046
32433
  priority: external_exports3.string().max(MAX_METADATA_LENGTH).optional(),
@@ -32052,10 +32439,14 @@ function registerTools(server, backend) {
32052
32439
  bodyAppend: external_exports3.string().max(MAX_DESCRIPTION_LENGTH).optional(),
32053
32440
  bodyReplace: external_exports3.array(external_exports3.object({ old: external_exports3.string().max(MAX_DESCRIPTION_LENGTH), new: external_exports3.string().max(MAX_DESCRIPTION_LENGTH) })).optional(),
32054
32441
  ifMatch: external_exports3.string().max(MAX_METADATA_LENGTH).optional()
32442
+ }).superRefine((input, ctx) => {
32443
+ if (!input.beanId) {
32444
+ ctx.addIssue({ code: external_exports3.ZodIssueCode.custom, path: ["beanId"], message: BEAN_ID_HINT });
32445
+ }
32055
32446
  }).refine(
32056
32447
  (input) => !(input.body !== void 0 && (input.bodyAppend !== void 0 || input.bodyReplace !== void 0)),
32057
32448
  { message: "body cannot be combined with bodyAppend/bodyReplace" }
32058
- );
32449
+ ).transform((input) => ({ ...input, beanId: input.beanId }));
32059
32450
  server.registerTool(
32060
32451
  "beans_bulk_update",
32061
32452
  {
@@ -32074,25 +32465,56 @@ function registerTools(server, backend) {
32074
32465
  },
32075
32466
  bulkUpdateHandler(backend)
32076
32467
  );
32468
+ server.registerTool(
32469
+ "beans_complete_tasks",
32470
+ {
32471
+ title: "Complete Markdown Tasks",
32472
+ description: "Mark all markdown checklist tasks within a bean as completed.",
32473
+ inputSchema: external_exports3.object({
32474
+ beanId: external_exports3.string().min(1).max(MAX_ID_LENGTH).optional()
32475
+ }).superRefine((input, ctx) => {
32476
+ if (!input.beanId) {
32477
+ ctx.addIssue({ code: external_exports3.ZodIssueCode.custom, path: ["beanId"], message: BEAN_ID_HINT });
32478
+ }
32479
+ }).transform((input) => ({ ...input, beanId: input.beanId })),
32480
+ annotations: {
32481
+ readOnlyHint: false,
32482
+ destructiveHint: false,
32483
+ idempotentHint: true,
32484
+ openWorldHint: false
32485
+ }
32486
+ },
32487
+ completeTasksHandler(backend)
32488
+ );
32077
32489
  server.registerTool(
32078
32490
  "beans_query",
32079
32491
  {
32080
32492
  title: "Query Beans",
32081
32493
  description: "Unified query tool for refresh, filter, search, and sort operations.",
32082
32494
  inputSchema: external_exports3.object({
32083
- operation: external_exports3.enum(["refresh", "filter", "search", "sort", "ready", "llm_context", "open_config"]).default("refresh"),
32495
+ operation: external_exports3.enum(["refresh", "filter", "search", "sort", "ready", "llm_context", "open_config", "graphql"]).default("refresh"),
32084
32496
  mode: external_exports3.enum(["status-priority-type-title", "updated", "created", "id"]).optional(),
32085
32497
  statuses: external_exports3.array(external_exports3.string().max(MAX_METADATA_LENGTH)).nullable().optional(),
32086
32498
  types: external_exports3.array(external_exports3.string().max(MAX_METADATA_LENGTH)).nullable().optional(),
32087
32499
  search: external_exports3.string().max(MAX_TITLE_LENGTH).optional(),
32088
32500
  includeClosed: external_exports3.boolean().optional(),
32089
32501
  tags: external_exports3.array(external_exports3.string().max(MAX_METADATA_LENGTH)).nullable().optional(),
32502
+ graphql: external_exports3.string().max(MAX_DESCRIPTION_LENGTH).optional(),
32503
+ variables: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional(),
32090
32504
  writeToWorkspaceInstructions: external_exports3.boolean().optional()
32505
+ }).superRefine((input, ctx) => {
32506
+ if (input.operation === "graphql" && (!input.graphql || input.graphql.trim().length === 0)) {
32507
+ ctx.addIssue({
32508
+ code: external_exports3.ZodIssueCode.custom,
32509
+ path: ["graphql"],
32510
+ message: "graphql query string is required when operation is graphql"
32511
+ });
32512
+ }
32091
32513
  }),
32092
32514
  annotations: {
32093
- readOnlyHint: true,
32515
+ readOnlyHint: false,
32094
32516
  destructiveHint: false,
32095
- idempotentHint: true,
32517
+ idempotentHint: false,
32096
32518
  openWorldHint: false
32097
32519
  }
32098
32520
  },
@@ -32104,10 +32526,33 @@ function registerTools(server, backend) {
32104
32526
  title: "Bean File Operations",
32105
32527
  description: "Read, create, edit, or delete files under .beans (operation param).",
32106
32528
  inputSchema: external_exports3.object({
32107
- operation: external_exports3.enum(["read", "edit", "create", "delete"]),
32529
+ operation: external_exports3.enum(["read", "edit", "create", "delete", "update_frontmatter"]),
32108
32530
  path: external_exports3.string().min(1).max(MAX_PATH_LENGTH),
32109
32531
  content: external_exports3.string().max(MAX_DESCRIPTION_LENGTH).optional(),
32110
- overwrite: external_exports3.boolean().optional()
32532
+ overwrite: external_exports3.boolean().optional(),
32533
+ fields: external_exports3.object({
32534
+ title: external_exports3.string().max(MAX_TITLE_LENGTH).optional(),
32535
+ status: external_exports3.string().max(MAX_METADATA_LENGTH).optional(),
32536
+ type: external_exports3.string().max(MAX_METADATA_LENGTH).optional(),
32537
+ priority: external_exports3.string().max(MAX_METADATA_LENGTH).optional(),
32538
+ parent_id: external_exports3.string().max(MAX_ID_LENGTH).nullable().optional(),
32539
+ tags: external_exports3.array(external_exports3.string().max(MAX_METADATA_LENGTH)).nullable().optional(),
32540
+ blocking_ids: external_exports3.array(external_exports3.string().max(MAX_ID_LENGTH)).nullable().optional(),
32541
+ blocked_by_ids: external_exports3.array(external_exports3.string().max(MAX_ID_LENGTH)).nullable().optional(),
32542
+ pr: external_exports3.string().max(MAX_TITLE_LENGTH).nullable().optional(),
32543
+ branch: external_exports3.string().max(MAX_TITLE_LENGTH).nullable().optional()
32544
+ }).optional()
32545
+ }).superRefine((input, ctx) => {
32546
+ if (input.operation === "update_frontmatter") {
32547
+ const fieldCount = Object.values(input.fields || {}).filter((value) => value !== void 0).length;
32548
+ if (fieldCount === 0) {
32549
+ ctx.addIssue({
32550
+ code: external_exports3.ZodIssueCode.custom,
32551
+ path: ["fields"],
32552
+ message: "At least one frontmatter field update is required"
32553
+ });
32554
+ }
32555
+ }
32111
32556
  }),
32112
32557
  annotations: {
32113
32558
  readOnlyHint: false,
@@ -32147,6 +32592,18 @@ var MutableBackend = class {
32147
32592
  init(prefix) {
32148
32593
  return this.inner.init(prefix);
32149
32594
  }
32595
+ archive() {
32596
+ if (typeof this.inner.archive === "function") {
32597
+ return this.inner.archive();
32598
+ }
32599
+ throw new TypeError("Archive is not supported by backend");
32600
+ }
32601
+ queryGraphql(query, variables) {
32602
+ if (typeof this.inner.queryGraphql === "function") {
32603
+ return this.inner.queryGraphql(query, variables);
32604
+ }
32605
+ throw new TypeError("GraphQL passthrough is not supported by backend");
32606
+ }
32150
32607
  list(opts) {
32151
32608
  return this.inner.list(opts);
32152
32609
  }
@@ -32186,6 +32643,9 @@ var MutableBackend = class {
32186
32643
  editBeanFile(path, content) {
32187
32644
  return this.inner.editBeanFile(path, content);
32188
32645
  }
32646
+ updateBeanFrontmatter(path, updates) {
32647
+ return this.inner.updateBeanFrontmatter(path, updates);
32648
+ }
32189
32649
  createBeanFile(path, content, opts) {
32190
32650
  return this.inner.createBeanFile(path, content, opts);
32191
32651
  }
@@ -32252,6 +32712,16 @@ function parseCliArgs(argv) {
32252
32712
  const envPort = Number.parseInt(process.env.BEANS_VSCODE_MCP_PORT || process.env.BEANS_MCP_PORT || "", 10);
32253
32713
  let port = Number.isInteger(envPort) && envPort > 0 ? envPort : DEFAULT_MCP_PORT;
32254
32714
  let logDir;
32715
+ const parseStrictPositiveInt = (raw, flagName) => {
32716
+ if (!/^\d+$/.test(raw)) {
32717
+ throw new Error(`Invalid value for ${flagName}: ${raw}`);
32718
+ }
32719
+ const parsed = Number.parseInt(raw, 10);
32720
+ if (!Number.isInteger(parsed) || parsed <= 0) {
32721
+ throw new Error(`Invalid value for ${flagName}: ${raw}`);
32722
+ }
32723
+ return parsed;
32724
+ };
32255
32725
  for (let i = 0; i < argv.length; i += 1) {
32256
32726
  const arg = argv[i];
32257
32727
  if ((arg === "--workspace" || arg === "--workspace-root") && argv[i + 1]) {
@@ -32265,10 +32735,7 @@ function parseCliArgs(argv) {
32265
32735
  }
32266
32736
  i += 1;
32267
32737
  } else if (arg === "--port" && argv[i + 1]) {
32268
- const parsedPort = Number.parseInt(argv[i + 1], 10);
32269
- if (Number.isInteger(parsedPort) && parsedPort > 0) {
32270
- port = parsedPort;
32271
- }
32738
+ port = parseStrictPositiveInt(argv[i + 1], "--port");
32272
32739
  i += 1;
32273
32740
  } else if (arg === "--log-dir" && argv[i + 1]) {
32274
32741
  logDir = argv[i + 1];