@levnikolaevich/hex-line-mcp 1.29.0 → 1.30.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.mjs +160 -15
  2. package/package.json +1 -1
package/dist/server.mjs CHANGED
@@ -202,11 +202,12 @@ function readText(filePath) {
202
202
  // lib/security.mjs
203
203
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
204
204
  var EXTERNAL_SAFE_FOLDERS = [
205
- ".hex-skills/",
206
- ".claude/"
205
+ ".hex-skills",
206
+ ".claude"
207
207
  ];
208
208
  function isInExternalSafeFolder(absPath) {
209
- return EXTERNAL_SAFE_FOLDERS.some((folder) => absPath.includes(folder));
209
+ const parts = normalizeScopeValue(absPath).split("/").filter(Boolean);
210
+ return EXTERNAL_SAFE_FOLDERS.some((folder) => parts.includes(folder));
210
211
  }
211
212
  function normalizePath(p) {
212
213
  if (process.platform === "win32") {
@@ -5008,6 +5009,85 @@ OUTPUT_CAPPED: Output exceeded ${MAX_BULK_OUTPUT_CHARS} chars.`;
5008
5009
  return output;
5009
5010
  }
5010
5011
 
5012
+ // ../hex-common/src/runtime/error-classifier.mjs
5013
+ var FAILURE_CLASS = Object.freeze({
5014
+ NONE: "none",
5015
+ TIMEOUT_IDLE: "timeout_idle",
5016
+ TIMEOUT_PRODUCTIVE: "timeout_productive",
5017
+ PERMISSION_DENIAL: "permission_denial",
5018
+ TOOL_MISSING: "tool_missing",
5019
+ AUTH_MISSING: "auth_missing",
5020
+ RATE_LIMITED: "rate_limited",
5021
+ ASKED_QUESTION: "asked_question",
5022
+ AGENT_ERROR: "agent_error",
5023
+ UNKNOWN: "unknown"
5024
+ });
5025
+ var INPUT_ERROR_CODES = /* @__PURE__ */ new Set([
5026
+ "BAD_INPUT",
5027
+ "BAD_PATH",
5028
+ "BAD_REMOTE_PLATFORM",
5029
+ "INVALID_INPUT",
5030
+ "INVALID_EDIT_PAYLOAD",
5031
+ "INVALID_JSON",
5032
+ "PATH_OUTSIDE_ROOT",
5033
+ "PATH_NOT_FOUND",
5034
+ "FILE_NOT_FOUND",
5035
+ "FILE_OUTSIDE_PROJECT",
5036
+ "OUT_OF_RANGE",
5037
+ "UNSUPPORTED_REMOTE_PLATFORM"
5038
+ ]);
5039
+ var PERMISSION_ERROR_CODES = /* @__PURE__ */ new Set([
5040
+ "SSH_HOST_NOT_ALLOWED",
5041
+ "BLOCKED_COMMAND",
5042
+ "REMOTE_SSH_DISABLED"
5043
+ ]);
5044
+ var AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
5045
+ "SSH_AUTH_FAILED",
5046
+ "SSH_AUTH_MISSING",
5047
+ "SSH_KEY_UNREADABLE"
5048
+ ]);
5049
+ var TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
5050
+ "SSH_EXEC_TIMEOUT",
5051
+ "SSH_CONNECT_TIMEOUT",
5052
+ "EXEC_TIMEOUT",
5053
+ "TRANSFER_TIMEOUT"
5054
+ ]);
5055
+ function textFor(input) {
5056
+ return [
5057
+ input?.code,
5058
+ input?.message,
5059
+ input?.recovery,
5060
+ input?.stderr,
5061
+ input?.error
5062
+ ].filter(Boolean).map(String).join("\n");
5063
+ }
5064
+ function classifyMcpFailure(input = {}) {
5065
+ const code = String(input.code || "").toUpperCase();
5066
+ const text = textFor(input).toLowerCase();
5067
+ if (/\b(429|rate limit|too many requests|quota exceeded|throttl)/i.test(text) || code.includes("RATE_LIMIT")) {
5068
+ return { failure_class: FAILURE_CLASS.RATE_LIMITED, next_action: "defer_retry" };
5069
+ }
5070
+ if (/\b(auth|authentication|unauthorized|not authorized|login required|credential|token missing|no user for host|permission denied \(publickey\)|publickey)\b/i.test(text) || code.includes("AUTH") || AUTH_ERROR_CODES.has(code)) {
5071
+ return { failure_class: FAILURE_CLASS.AUTH_MISSING, next_action: "authenticate" };
5072
+ }
5073
+ if (/\b(eacces|eperm|permission denied|access denied|operation not permitted|forbidden)\b/i.test(text) || code === "GRAPH_DB_UNREADABLE" || PERMISSION_ERROR_CODES.has(code)) {
5074
+ return { failure_class: FAILURE_CLASS.PERMISSION_DENIAL, next_action: "fix_permissions" };
5075
+ }
5076
+ if (/\b(enoent|command not found|not recognized as|not found in path|required tool|provider setup failed|missing provider|tool missing)\b/i.test(text) || code === "GRAPH_PROVIDER_SETUP_FAILED") {
5077
+ return { failure_class: FAILURE_CLASS.TOOL_MISSING, next_action: "install_tool" };
5078
+ }
5079
+ if (/\b(etimedout|timed out|timeout|transfer_timeout|connection timed out|database is locked|busy or locked)\b/i.test(text) || code.includes("TIMEOUT") || code === "GRAPH_DB_BUSY" || TIMEOUT_ERROR_CODES.has(code)) {
5080
+ return { failure_class: FAILURE_CLASS.TIMEOUT_IDLE, next_action: "retry_after_wait" };
5081
+ }
5082
+ if (/\?\s*$|\b(please confirm|confirm\?|choose one|which option)\b/i.test(text)) {
5083
+ return { failure_class: FAILURE_CLASS.ASKED_QUESTION, next_action: "fix_inputs" };
5084
+ }
5085
+ if (INPUT_ERROR_CODES.has(code) || code.startsWith("INVALID_") || code.endsWith("_REQUIRED")) {
5086
+ return { failure_class: FAILURE_CLASS.UNKNOWN, next_action: "fix_inputs" };
5087
+ }
5088
+ return { failure_class: FAILURE_CLASS.UNKNOWN, next_action: "fix_inputs" };
5089
+ }
5090
+
5011
5091
  // ../hex-common/src/runtime/results.mjs
5012
5092
  var LARGE_RESULT_META = { "anthropic/maxResultSizeChars": 5e5 };
5013
5093
  function result(structured, { large = false } = {}) {
@@ -5021,9 +5101,26 @@ function result(structured, { large = false } = {}) {
5021
5101
  return response;
5022
5102
  }
5023
5103
  function errorResult(code, message, recovery, { large = false, extra = null } = {}) {
5104
+ const normalizedCode = String(code || "ERROR");
5105
+ const normalizedMessage = String(message || "Unknown MCP tool error");
5106
+ const normalizedRecovery = String(recovery || "Review the error and retry with corrected inputs");
5107
+ const classification = classifyMcpFailure({
5108
+ code: normalizedCode,
5109
+ message: normalizedMessage,
5110
+ recovery: normalizedRecovery
5111
+ });
5024
5112
  const payload = {
5025
5113
  status: "ERROR",
5026
- error: { code, message, recovery }
5114
+ code: normalizedCode,
5115
+ summary: normalizedMessage,
5116
+ next_action: classification.next_action,
5117
+ recovery: normalizedRecovery,
5118
+ failure_class: classification.failure_class,
5119
+ error: {
5120
+ code: normalizedCode,
5121
+ message: normalizedMessage,
5122
+ recovery: normalizedRecovery
5123
+ }
5027
5124
  };
5028
5125
  if (extra && typeof extra === "object") {
5029
5126
  Object.assign(payload, extra);
@@ -5032,9 +5129,17 @@ function errorResult(code, message, recovery, { large = false, extra = null } =
5032
5129
  }
5033
5130
 
5034
5131
  // server.mjs
5035
- var version = true ? "1.29.0" : (await null).createRequire(import.meta.url)("./package.json").version;
5132
+ var version = true ? "1.30.0" : (await null).createRequire(import.meta.url)("./package.json").version;
5036
5133
  var STATUS_ENUM = z2.enum(STATUS_VALUES);
5037
5134
  var ERROR_SHAPE = z2.object({ code: z2.string(), message: z2.string(), recovery: z2.string() }).optional();
5135
+ var ERROR_RESULT_FIELDS = {
5136
+ code: z2.string().optional(),
5137
+ summary: z2.string().optional(),
5138
+ next_action: z2.string().optional(),
5139
+ recovery: z2.string().optional(),
5140
+ failure_class: z2.string().optional(),
5141
+ error: ERROR_SHAPE
5142
+ };
5038
5143
  var LINE_REPORT_KEYS = /* @__PURE__ */ new Set([
5039
5144
  "status",
5040
5145
  "reason",
@@ -5055,6 +5160,46 @@ var LINE_REPORT_KEYS = /* @__PURE__ */ new Set([
5055
5160
  "remapped_refs",
5056
5161
  "warnings"
5057
5162
  ]);
5163
+ var EDIT_PAYLOAD_TYPES = ["set_line", "insert_after", "replace_lines", "replace_between"];
5164
+ var EDIT_REQUIRED_FIELDS = {
5165
+ set_line: ["anchor", "new_text"],
5166
+ insert_after: ["anchor", "text"],
5167
+ replace_lines: ["start_anchor", "end_anchor", "new_text"],
5168
+ replace_between: ["start_anchor", "end_anchor", "new_text"]
5169
+ };
5170
+ function inputError(code, message, recovery) {
5171
+ const error = new Error(message);
5172
+ error.code = code;
5173
+ error.recovery = recovery;
5174
+ return error;
5175
+ }
5176
+ function validateEditPayload(edits) {
5177
+ if (!Array.isArray(edits) || edits.length === 0) {
5178
+ throw inputError("INVALID_EDIT_PAYLOAD", "BAD_INPUT: edits must be a non-empty JSON array", 'Pass canonical edit objects such as {"set_line":{"anchor":"ab.12","new_text":"..."}}');
5179
+ }
5180
+ edits.forEach((edit, index) => {
5181
+ if (!edit || typeof edit !== "object" || Array.isArray(edit)) {
5182
+ throw inputError("INVALID_EDIT_PAYLOAD", `BAD_INPUT: edit at index ${index} must be an object`, "Use one canonical edit object per array item");
5183
+ }
5184
+ const keys = EDIT_PAYLOAD_TYPES.filter((type2) => Object.prototype.hasOwnProperty.call(edit, type2));
5185
+ if (keys.length === 0) {
5186
+ throw inputError("INVALID_EDIT_PAYLOAD", `BAD_INPUT: unknown edit type at index ${index}`, "Use set_line, insert_after, replace_lines, or replace_between");
5187
+ }
5188
+ if (keys.length > 1) {
5189
+ throw inputError("INVALID_EDIT_PAYLOAD", `BAD_INPUT: edit at index ${index} has multiple edit types`, "Use exactly one edit type per object");
5190
+ }
5191
+ const [type] = keys;
5192
+ const payload = edit[type];
5193
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
5194
+ throw inputError("INVALID_EDIT_PAYLOAD", `BAD_INPUT: ${type} payload at index ${index} must be an object`, "Nest fields under the canonical edit type");
5195
+ }
5196
+ for (const field of EDIT_REQUIRED_FIELDS[type]) {
5197
+ if (typeof payload[field] !== "string") {
5198
+ throw inputError("INVALID_EDIT_PAYLOAD", `BAD_INPUT: ${type}.${field} must be a string at index ${index}`, "Provide all required canonical edit fields before retrying");
5199
+ }
5200
+ }
5201
+ });
5202
+ }
5058
5203
  var { server, StdioServerTransport } = await createServerRuntime({
5059
5204
  name: "hex-line-mcp",
5060
5205
  version
@@ -5126,7 +5271,7 @@ server.registerTool("read_file", {
5126
5271
  content: z2.string().optional(),
5127
5272
  edit_ready: z2.boolean().optional(),
5128
5273
  next_action: z2.string().optional(),
5129
- error: ERROR_SHAPE
5274
+ ...ERROR_RESULT_FIELDS
5130
5275
  }),
5131
5276
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
5132
5277
  }, async (rawParams) => {
@@ -5190,7 +5335,7 @@ server.registerTool("edit_file", {
5190
5335
  conflict_policy: z2.enum(["strict", "conservative"]).optional().describe('Conflict handling (default: "conservative"). "conservative" returns structured CONFLICT output with recovery_ranges, retry_edit/retry_edits, suggested_read_call, and retry_plan when available.'),
5191
5336
  allow_external: flexBool().describe("Allow editing a path outside the current project root. Use only when you intentionally target a temp or external file.")
5192
5337
  }),
5193
- outputSchema: z2.object({ status: STATUS_ENUM, file_path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), summary: z2.string().optional(), next_action: z2.string().optional(), warnings: z2.array(z2.object({ code: z2.string() }).passthrough()).optional(), error: ERROR_SHAPE }),
5338
+ outputSchema: z2.object({ status: STATUS_ENUM, file_path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), warnings: z2.array(z2.object({ code: z2.string() }).passthrough()).optional(), ...ERROR_RESULT_FIELDS }),
5194
5339
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
5195
5340
  }, async (rawParams) => {
5196
5341
  const { file_path: p, edits: json, dry_run, restore_indent, base_revision, conflict_policy, allow_external } = rawParams ?? {};
@@ -5202,7 +5347,7 @@ server.registerTool("edit_file", {
5202
5347
  } catch {
5203
5348
  throw new Error('edits: invalid JSON. Expected: [{"set_line":{"anchor":"xx.N","new_text":"..."}}]');
5204
5349
  }
5205
- if (!Array.isArray(parsed) || !parsed.length) throw new Error("Edits: non-empty JSON array required");
5350
+ validateEditPayload(parsed);
5206
5351
  const content = editFile(p, parsed, {
5207
5352
  dryRun: dry_run,
5208
5353
  restoreIndent: restore_indent,
@@ -5222,7 +5367,7 @@ server.registerTool("write_file", {
5222
5367
  content: z2.string().describe("File content"),
5223
5368
  allow_external: flexBool().describe("Allow writing a path outside the current project root. Use only when you intentionally target a temp or external file.")
5224
5369
  }),
5225
- outputSchema: z2.object({ status: STATUS_ENUM, file_path: z2.string().optional(), lines: z2.number().optional(), error: ERROR_SHAPE }),
5370
+ outputSchema: z2.object({ status: STATUS_ENUM, file_path: z2.string().optional(), lines: z2.number().optional(), ...ERROR_RESULT_FIELDS }),
5226
5371
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
5227
5372
  }, async (rawParams) => {
5228
5373
  const { file_path: p, content, allow_external } = rawParams ?? {};
@@ -5259,7 +5404,7 @@ server.registerTool("grep_search", {
5259
5404
  edit_ready: flexBool().describe("Preserve hash/checksum search hunks in `content` mode. Default: false."),
5260
5405
  allow_large_output: flexBool().describe("Bypass the default content-mode block/char caps when you intentionally need a larger payload.")
5261
5406
  }),
5262
- outputSchema: z2.object({ status: STATUS_ENUM, pattern: z2.string().optional(), content: z2.string().optional(), next_action: z2.string().optional(), error: ERROR_SHAPE }),
5407
+ outputSchema: z2.object({ status: STATUS_ENUM, pattern: z2.string().optional(), content: z2.string().optional(), ...ERROR_RESULT_FIELDS }),
5263
5408
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
5264
5409
  }, async (rawParams) => {
5265
5410
  const {
@@ -5314,7 +5459,7 @@ server.registerTool("outline", {
5314
5459
  inputSchema: z2.object({
5315
5460
  file_path: z2.string().describe("Source file path")
5316
5461
  }),
5317
- outputSchema: z2.object({ status: STATUS_ENUM, file_path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), summary: z2.string().optional(), next_action: z2.string().optional(), error: ERROR_SHAPE }),
5462
+ outputSchema: z2.object({ status: STATUS_ENUM, file_path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), ...ERROR_RESULT_FIELDS }),
5318
5463
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
5319
5464
  }, async (rawParams) => {
5320
5465
  const { file_path: p } = rawParams ?? {};
@@ -5333,7 +5478,7 @@ server.registerTool("verify", {
5333
5478
  checksums: z2.array(z2.string()).describe('Checksum strings, e.g. ["1-50:f7e2a1b0", "51-100:abcd1234"]'),
5334
5479
  base_revision: z2.string().optional().describe("Optional prior revision to compare against latest state.")
5335
5480
  }),
5336
- outputSchema: z2.object({ status: STATUS_ENUM, file_path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), summary: z2.string().optional(), next_action: z2.string().optional(), error: ERROR_SHAPE }),
5481
+ outputSchema: z2.object({ status: STATUS_ENUM, file_path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), ...ERROR_RESULT_FIELDS }),
5337
5482
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
5338
5483
  }, async (rawParams) => {
5339
5484
  const { file_path: p, checksums, base_revision } = rawParams ?? {};
@@ -5360,7 +5505,7 @@ server.registerTool("inspect_path", {
5360
5505
  format: z2.enum(["compact", "full"]).optional().describe('"compact" = shorter path view, "full" = include sizes/metadata where available'),
5361
5506
  verbosity: z2.enum(["minimal", "compact", "full"]).optional().describe("Response budget. `minimal` returns the shortest tree summary.")
5362
5507
  }),
5363
- outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), summary: z2.string().optional(), next_action: z2.string().optional(), error: ERROR_SHAPE }),
5508
+ outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), ...ERROR_RESULT_FIELDS }),
5364
5509
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
5365
5510
  }, async (rawParams) => {
5366
5511
  const { path: p, max_depth, max_entries, gitignore, format, pattern, type: entryType, verbosity } = rawParams ?? {};
@@ -5386,7 +5531,7 @@ server.registerTool("changes", {
5386
5531
  path: z2.string().describe("File or directory path"),
5387
5532
  compare_against: z2.string().optional().describe('Git ref to compare against (default: "HEAD")')
5388
5533
  }),
5389
- outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), next_action: z2.string().optional(), error: ERROR_SHAPE }),
5534
+ outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), ...ERROR_RESULT_FIELDS }),
5390
5535
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
5391
5536
  }, async (rawParams) => {
5392
5537
  const { path: p, compare_against } = rawParams ?? {};
@@ -5409,7 +5554,7 @@ server.registerTool("bulk_replace", {
5409
5554
  format: z2.enum(["compact", "full"]).optional().describe('"compact" (default) = summary only, "full" = include capped diffs'),
5410
5555
  allow_external: flexBool().describe("Allow a replacement root outside the current project root. Use only when you intentionally target a temp or external directory.")
5411
5556
  }),
5412
- outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), summary: z2.string().optional(), next_action: z2.string().optional(), error: ERROR_SHAPE }),
5557
+ outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), ...ERROR_RESULT_FIELDS }),
5413
5558
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
5414
5559
  }, async (rawParams) => {
5415
5560
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levnikolaevich/hex-line-mcp",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "mcpName": "io.github.levnikolaevich/hex-line-mcp",
5
5
  "type": "module",
6
6
  "description": "Hash-verified file editing MCP + token efficiency hook for AI coding agents. 9 tools: inspect_path, read, edit, write, grep, outline, verify, changes, bulk_replace.",