@levnikolaevich/hex-line-mcp 1.29.0 → 1.30.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.
Files changed (2) hide show
  1. package/dist/server.mjs +163 -17
  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,22 +5009,119 @@ 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
- function result(structured, { large = false } = {}) {
5093
+ function result(structured, { large = false, isError = null, errorStatuses = ["ERROR"] } = {}) {
5014
5094
  const text = JSON.stringify(structured);
5015
5095
  const response = {
5016
5096
  content: [{ type: "text", text }],
5017
5097
  structuredContent: structured
5018
5098
  };
5019
5099
  if (large) response._meta = LARGE_RESULT_META;
5020
- if (structured.status === "ERROR") response.isError = true;
5100
+ const resolvedError = isError === null ? new Set(errorStatuses).has(structured?.status) : isError;
5101
+ if (resolvedError) response.isError = true;
5021
5102
  return response;
5022
5103
  }
5023
5104
  function errorResult(code, message, recovery, { large = false, extra = null } = {}) {
5105
+ const normalizedCode = String(code || "ERROR");
5106
+ const normalizedMessage = String(message || "Unknown MCP tool error");
5107
+ const normalizedRecovery = String(recovery || "Review the error and retry with corrected inputs");
5108
+ const classification = classifyMcpFailure({
5109
+ code: normalizedCode,
5110
+ message: normalizedMessage,
5111
+ recovery: normalizedRecovery
5112
+ });
5024
5113
  const payload = {
5025
5114
  status: "ERROR",
5026
- error: { code, message, recovery }
5115
+ code: normalizedCode,
5116
+ summary: normalizedMessage,
5117
+ next_action: classification.next_action,
5118
+ recovery: normalizedRecovery,
5119
+ failure_class: classification.failure_class,
5120
+ error: {
5121
+ code: normalizedCode,
5122
+ message: normalizedMessage,
5123
+ recovery: normalizedRecovery
5124
+ }
5027
5125
  };
5028
5126
  if (extra && typeof extra === "object") {
5029
5127
  Object.assign(payload, extra);
@@ -5032,9 +5130,17 @@ function errorResult(code, message, recovery, { large = false, extra = null } =
5032
5130
  }
5033
5131
 
5034
5132
  // server.mjs
5035
- var version = true ? "1.29.0" : (await null).createRequire(import.meta.url)("./package.json").version;
5133
+ var version = true ? "1.30.1" : (await null).createRequire(import.meta.url)("./package.json").version;
5036
5134
  var STATUS_ENUM = z2.enum(STATUS_VALUES);
5037
5135
  var ERROR_SHAPE = z2.object({ code: z2.string(), message: z2.string(), recovery: z2.string() }).optional();
5136
+ var ERROR_RESULT_FIELDS = {
5137
+ code: z2.string().optional(),
5138
+ summary: z2.string().optional(),
5139
+ next_action: z2.string().optional(),
5140
+ recovery: z2.string().optional(),
5141
+ failure_class: z2.string().optional(),
5142
+ error: ERROR_SHAPE
5143
+ };
5038
5144
  var LINE_REPORT_KEYS = /* @__PURE__ */ new Set([
5039
5145
  "status",
5040
5146
  "reason",
@@ -5055,6 +5161,46 @@ var LINE_REPORT_KEYS = /* @__PURE__ */ new Set([
5055
5161
  "remapped_refs",
5056
5162
  "warnings"
5057
5163
  ]);
5164
+ var EDIT_PAYLOAD_TYPES = ["set_line", "insert_after", "replace_lines", "replace_between"];
5165
+ var EDIT_REQUIRED_FIELDS = {
5166
+ set_line: ["anchor", "new_text"],
5167
+ insert_after: ["anchor", "text"],
5168
+ replace_lines: ["start_anchor", "end_anchor", "new_text"],
5169
+ replace_between: ["start_anchor", "end_anchor", "new_text"]
5170
+ };
5171
+ function inputError(code, message, recovery) {
5172
+ const error = new Error(message);
5173
+ error.code = code;
5174
+ error.recovery = recovery;
5175
+ return error;
5176
+ }
5177
+ function validateEditPayload(edits) {
5178
+ if (!Array.isArray(edits) || edits.length === 0) {
5179
+ 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":"..."}}');
5180
+ }
5181
+ edits.forEach((edit, index) => {
5182
+ if (!edit || typeof edit !== "object" || Array.isArray(edit)) {
5183
+ throw inputError("INVALID_EDIT_PAYLOAD", `BAD_INPUT: edit at index ${index} must be an object`, "Use one canonical edit object per array item");
5184
+ }
5185
+ const keys = EDIT_PAYLOAD_TYPES.filter((type2) => Object.prototype.hasOwnProperty.call(edit, type2));
5186
+ if (keys.length === 0) {
5187
+ throw inputError("INVALID_EDIT_PAYLOAD", `BAD_INPUT: unknown edit type at index ${index}`, "Use set_line, insert_after, replace_lines, or replace_between");
5188
+ }
5189
+ if (keys.length > 1) {
5190
+ throw inputError("INVALID_EDIT_PAYLOAD", `BAD_INPUT: edit at index ${index} has multiple edit types`, "Use exactly one edit type per object");
5191
+ }
5192
+ const [type] = keys;
5193
+ const payload = edit[type];
5194
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
5195
+ throw inputError("INVALID_EDIT_PAYLOAD", `BAD_INPUT: ${type} payload at index ${index} must be an object`, "Nest fields under the canonical edit type");
5196
+ }
5197
+ for (const field of EDIT_REQUIRED_FIELDS[type]) {
5198
+ if (typeof payload[field] !== "string") {
5199
+ throw inputError("INVALID_EDIT_PAYLOAD", `BAD_INPUT: ${type}.${field} must be a string at index ${index}`, "Provide all required canonical edit fields before retrying");
5200
+ }
5201
+ }
5202
+ });
5203
+ }
5058
5204
  var { server, StdioServerTransport } = await createServerRuntime({
5059
5205
  name: "hex-line-mcp",
5060
5206
  version
@@ -5126,7 +5272,7 @@ server.registerTool("read_file", {
5126
5272
  content: z2.string().optional(),
5127
5273
  edit_ready: z2.boolean().optional(),
5128
5274
  next_action: z2.string().optional(),
5129
- error: ERROR_SHAPE
5275
+ ...ERROR_RESULT_FIELDS
5130
5276
  }),
5131
5277
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
5132
5278
  }, async (rawParams) => {
@@ -5190,7 +5336,7 @@ server.registerTool("edit_file", {
5190
5336
  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
5337
  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
5338
  }),
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 }),
5339
+ 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
5340
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
5195
5341
  }, async (rawParams) => {
5196
5342
  const { file_path: p, edits: json, dry_run, restore_indent, base_revision, conflict_policy, allow_external } = rawParams ?? {};
@@ -5202,7 +5348,7 @@ server.registerTool("edit_file", {
5202
5348
  } catch {
5203
5349
  throw new Error('edits: invalid JSON. Expected: [{"set_line":{"anchor":"xx.N","new_text":"..."}}]');
5204
5350
  }
5205
- if (!Array.isArray(parsed) || !parsed.length) throw new Error("Edits: non-empty JSON array required");
5351
+ validateEditPayload(parsed);
5206
5352
  const content = editFile(p, parsed, {
5207
5353
  dryRun: dry_run,
5208
5354
  restoreIndent: restore_indent,
@@ -5222,7 +5368,7 @@ server.registerTool("write_file", {
5222
5368
  content: z2.string().describe("File content"),
5223
5369
  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
5370
  }),
5225
- outputSchema: z2.object({ status: STATUS_ENUM, file_path: z2.string().optional(), lines: z2.number().optional(), error: ERROR_SHAPE }),
5371
+ outputSchema: z2.object({ status: STATUS_ENUM, file_path: z2.string().optional(), lines: z2.number().optional(), ...ERROR_RESULT_FIELDS }),
5226
5372
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
5227
5373
  }, async (rawParams) => {
5228
5374
  const { file_path: p, content, allow_external } = rawParams ?? {};
@@ -5259,7 +5405,7 @@ server.registerTool("grep_search", {
5259
5405
  edit_ready: flexBool().describe("Preserve hash/checksum search hunks in `content` mode. Default: false."),
5260
5406
  allow_large_output: flexBool().describe("Bypass the default content-mode block/char caps when you intentionally need a larger payload.")
5261
5407
  }),
5262
- outputSchema: z2.object({ status: STATUS_ENUM, pattern: z2.string().optional(), content: z2.string().optional(), next_action: z2.string().optional(), error: ERROR_SHAPE }),
5408
+ outputSchema: z2.object({ status: STATUS_ENUM, pattern: z2.string().optional(), content: z2.string().optional(), ...ERROR_RESULT_FIELDS }),
5263
5409
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
5264
5410
  }, async (rawParams) => {
5265
5411
  const {
@@ -5314,7 +5460,7 @@ server.registerTool("outline", {
5314
5460
  inputSchema: z2.object({
5315
5461
  file_path: z2.string().describe("Source file path")
5316
5462
  }),
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 }),
5463
+ outputSchema: z2.object({ status: STATUS_ENUM, file_path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), ...ERROR_RESULT_FIELDS }),
5318
5464
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
5319
5465
  }, async (rawParams) => {
5320
5466
  const { file_path: p } = rawParams ?? {};
@@ -5333,7 +5479,7 @@ server.registerTool("verify", {
5333
5479
  checksums: z2.array(z2.string()).describe('Checksum strings, e.g. ["1-50:f7e2a1b0", "51-100:abcd1234"]'),
5334
5480
  base_revision: z2.string().optional().describe("Optional prior revision to compare against latest state.")
5335
5481
  }),
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 }),
5482
+ outputSchema: z2.object({ status: STATUS_ENUM, file_path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), ...ERROR_RESULT_FIELDS }),
5337
5483
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
5338
5484
  }, async (rawParams) => {
5339
5485
  const { file_path: p, checksums, base_revision } = rawParams ?? {};
@@ -5360,7 +5506,7 @@ server.registerTool("inspect_path", {
5360
5506
  format: z2.enum(["compact", "full"]).optional().describe('"compact" = shorter path view, "full" = include sizes/metadata where available'),
5361
5507
  verbosity: z2.enum(["minimal", "compact", "full"]).optional().describe("Response budget. `minimal` returns the shortest tree summary.")
5362
5508
  }),
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 }),
5509
+ outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), ...ERROR_RESULT_FIELDS }),
5364
5510
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
5365
5511
  }, async (rawParams) => {
5366
5512
  const { path: p, max_depth, max_entries, gitignore, format, pattern, type: entryType, verbosity } = rawParams ?? {};
@@ -5386,7 +5532,7 @@ server.registerTool("changes", {
5386
5532
  path: z2.string().describe("File or directory path"),
5387
5533
  compare_against: z2.string().optional().describe('Git ref to compare against (default: "HEAD")')
5388
5534
  }),
5389
- outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), next_action: z2.string().optional(), error: ERROR_SHAPE }),
5535
+ outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), ...ERROR_RESULT_FIELDS }),
5390
5536
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
5391
5537
  }, async (rawParams) => {
5392
5538
  const { path: p, compare_against } = rawParams ?? {};
@@ -5409,7 +5555,7 @@ server.registerTool("bulk_replace", {
5409
5555
  format: z2.enum(["compact", "full"]).optional().describe('"compact" (default) = summary only, "full" = include capped diffs'),
5410
5556
  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
5557
  }),
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 }),
5558
+ outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), reason: z2.string().optional(), ...ERROR_RESULT_FIELDS }),
5413
5559
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
5414
5560
  }, async (rawParams) => {
5415
5561
  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.1",
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.",