opencode-hashline 1.0.0 → 1.0.2

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.
package/README.md CHANGED
@@ -5,8 +5,12 @@
5
5
  **Content-addressable line hashing for precise AI code editing**
6
6
 
7
7
  [![CI](https://github.com/izzzzzi/opencode-hashline/actions/workflows/ci.yml/badge.svg)](https://github.com/izzzzzi/opencode-hashline/actions/workflows/ci.yml)
8
+ [![Release](https://github.com/izzzzzi/opencode-hashline/actions/workflows/release.yml/badge.svg)](https://github.com/izzzzzi/opencode-hashline/actions/workflows/release.yml)
8
9
  [![npm version](https://img.shields.io/npm/v/opencode-hashline.svg?style=flat&colorA=18181B&colorB=28CF8D)](https://www.npmjs.com/package/opencode-hashline)
10
+ [![npm downloads](https://img.shields.io/npm/dm/opencode-hashline.svg?style=flat&colorA=18181B&colorB=28CF8D)](https://www.npmjs.com/package/opencode-hashline)
11
+ [![GitHub release](https://img.shields.io/github/v/release/izzzzzi/opencode-hashline?style=flat&colorA=18181B&colorB=28CF8D)](https://github.com/izzzzzi/opencode-hashline/releases)
9
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat&colorA=18181B&colorB=28CF8D)](LICENSE)
13
+ [![semantic-release](https://img.shields.io/badge/semantic--release-auto-e10079?style=flat&colorA=18181B)](https://github.com/semantic-release/semantic-release)
10
14
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat&colorA=18181B&colorB=3178C6)](https://www.typescriptlang.org/)
11
15
  [![Node.js](https://img.shields.io/badge/Node.js-ESM-green?style=flat&colorA=18181B&colorB=339933)](https://nodejs.org/)
12
16
 
package/README.ru.md CHANGED
@@ -5,8 +5,12 @@
5
5
  **Контентно-адресуемое хеширование строк для точного редактирования кода с помощью AI**
6
6
 
7
7
  [![CI](https://github.com/izzzzzi/opencode-hashline/actions/workflows/ci.yml/badge.svg)](https://github.com/izzzzzi/opencode-hashline/actions/workflows/ci.yml)
8
+ [![Release](https://github.com/izzzzzi/opencode-hashline/actions/workflows/release.yml/badge.svg)](https://github.com/izzzzzi/opencode-hashline/actions/workflows/release.yml)
8
9
  [![npm version](https://img.shields.io/npm/v/opencode-hashline.svg?style=flat&colorA=18181B&colorB=28CF8D)](https://www.npmjs.com/package/opencode-hashline)
10
+ [![npm downloads](https://img.shields.io/npm/dm/opencode-hashline.svg?style=flat&colorA=18181B&colorB=28CF8D)](https://www.npmjs.com/package/opencode-hashline)
11
+ [![GitHub release](https://img.shields.io/github/v/release/izzzzzi/opencode-hashline?style=flat&colorA=18181B&colorB=28CF8D)](https://github.com/izzzzzi/opencode-hashline/releases)
9
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat&colorA=18181B&colorB=28CF8D)](LICENSE)
13
+ [![semantic-release](https://img.shields.io/badge/semantic--release-auto-e10079?style=flat&colorA=18181B)](https://github.com/semantic-release/semantic-release)
10
14
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat&colorA=18181B&colorB=3178C6)](https://www.typescriptlang.org/)
11
15
  [![Node.js](https://img.shields.io/badge/Node.js-ESM-green?style=flat&colorA=18181B&colorB=339933)](https://nodejs.org/)
12
16
 
@@ -20,7 +20,7 @@ function debug(...args) {
20
20
  }
21
21
  }
22
22
  var FILE_READ_TOOLS = ["read", "file_read", "read_file", "cat", "view"];
23
- var FILE_EDIT_TOOLS = ["write", "file_write", "file_edit", "edit", "edit_file", "patch", "apply_patch", "multiedit"];
23
+ var FILE_EDIT_TOOLS = ["write", "file_write", "file_edit", "edit", "edit_file", "patch", "apply_patch", "multiedit", "batch"];
24
24
  function isFileReadTool(toolName, args) {
25
25
  const lower = toolName.toLowerCase();
26
26
  const nameMatch = FILE_READ_TOOLS.some(
@@ -40,8 +40,16 @@ function createFileReadAfterHook(cache, config) {
40
40
  const resolved = config ?? resolveConfig();
41
41
  const hashLen = resolved.hashLength || 0;
42
42
  const prefix = resolved.prefix;
43
+ const processedCallIds = /* @__PURE__ */ new Set();
43
44
  return async (input, output) => {
44
45
  debug("tool.execute.after:", input.tool, "args:", input.args);
46
+ if (input.callID) {
47
+ if (processedCallIds.has(input.callID)) {
48
+ debug("skipped: duplicate callID", input.callID);
49
+ return;
50
+ }
51
+ processedCallIds.add(input.callID);
52
+ }
45
53
  if (!isFileReadTool(input.tool, input.args)) {
46
54
  debug("skipped: not a file-read tool");
47
55
  return;
@@ -79,7 +87,14 @@ function createFileReadAfterHook(cache, config) {
79
87
  function createFileEditBeforeHook(config) {
80
88
  const resolved = config ?? resolveConfig();
81
89
  const prefix = resolved.prefix;
90
+ const processedCallIds = /* @__PURE__ */ new Set();
82
91
  return async (input, output) => {
92
+ if (input.callID) {
93
+ if (processedCallIds.has(input.callID)) {
94
+ return;
95
+ }
96
+ processedCallIds.add(input.callID);
97
+ }
83
98
  const toolName = input.tool.toLowerCase();
84
99
  const isFileEdit = FILE_EDIT_TOOLS.some(
85
100
  (name) => toolName === name || toolName.endsWith(`.${name}`)
package/dist/index.cjs CHANGED
@@ -476,7 +476,7 @@ function debug(...args) {
476
476
  }
477
477
  }
478
478
  var FILE_READ_TOOLS = ["read", "file_read", "read_file", "cat", "view"];
479
- var FILE_EDIT_TOOLS = ["write", "file_write", "file_edit", "edit", "edit_file", "patch", "apply_patch", "multiedit"];
479
+ var FILE_EDIT_TOOLS = ["write", "file_write", "file_edit", "edit", "edit_file", "patch", "apply_patch", "multiedit", "batch"];
480
480
  function isFileReadTool(toolName, args) {
481
481
  const lower = toolName.toLowerCase();
482
482
  const nameMatch = FILE_READ_TOOLS.some(
@@ -496,8 +496,16 @@ function createFileReadAfterHook(cache, config) {
496
496
  const resolved = config ?? resolveConfig();
497
497
  const hashLen = resolved.hashLength || 0;
498
498
  const prefix = resolved.prefix;
499
+ const processedCallIds = /* @__PURE__ */ new Set();
499
500
  return async (input, output) => {
500
501
  debug("tool.execute.after:", input.tool, "args:", input.args);
502
+ if (input.callID) {
503
+ if (processedCallIds.has(input.callID)) {
504
+ debug("skipped: duplicate callID", input.callID);
505
+ return;
506
+ }
507
+ processedCallIds.add(input.callID);
508
+ }
501
509
  if (!isFileReadTool(input.tool, input.args)) {
502
510
  debug("skipped: not a file-read tool");
503
511
  return;
@@ -535,7 +543,14 @@ function createFileReadAfterHook(cache, config) {
535
543
  function createFileEditBeforeHook(config) {
536
544
  const resolved = config ?? resolveConfig();
537
545
  const prefix = resolved.prefix;
546
+ const processedCallIds = /* @__PURE__ */ new Set();
538
547
  return async (input, output) => {
548
+ if (input.callID) {
549
+ if (processedCallIds.has(input.callID)) {
550
+ return;
551
+ }
552
+ processedCallIds.add(input.callID);
553
+ }
539
554
  const toolName = input.tool.toLowerCase();
540
555
  const isFileEdit = FILE_EDIT_TOOLS.some(
541
556
  (name) => toolName === name || toolName.endsWith(`.${name}`)
@@ -631,26 +646,27 @@ init_hashline();
631
646
  // src/hashline-tool.ts
632
647
  var import_fs2 = require("fs");
633
648
  var import_path2 = require("path");
634
- var import_tool = require("@opencode-ai/plugin/tool");
649
+ var import_zod = require("zod");
635
650
  init_hashline();
636
651
  function createHashlineEditTool(config, cache) {
637
- return (0, import_tool.tool)({
652
+ return {
638
653
  description: "Edit files using hashline references. Resolves refs like 5:a3f or '#HL 5:a3f|...' and applies replace/delete/insert without old_string matching.",
639
654
  args: {
640
- path: import_tool.tool.schema.string().describe("Path to the file (absolute or relative to project directory)"),
641
- operation: import_tool.tool.schema.enum(["replace", "delete", "insert_before", "insert_after"]).describe("Edit operation"),
642
- startRef: import_tool.tool.schema.string().describe('Start hash reference, e.g. "5:a3f" or "#HL 5:a3f|const x = 1;"'),
643
- endRef: import_tool.tool.schema.string().optional().describe("End hash reference for range operations. Defaults to startRef when omitted."),
644
- replacement: import_tool.tool.schema.string().optional().describe("Replacement/inserted content. Required for replace/insert operations.")
655
+ path: import_zod.z.string().describe("Path to the file (absolute or relative to project directory)"),
656
+ operation: import_zod.z.enum(["replace", "delete", "insert_before", "insert_after"]).describe("Edit operation"),
657
+ startRef: import_zod.z.string().describe('Start hash reference, e.g. "5:a3f" or "#HL 5:a3f|const x = 1;"'),
658
+ endRef: import_zod.z.string().optional().describe("End hash reference for range operations. Defaults to startRef when omitted."),
659
+ replacement: import_zod.z.string().optional().describe("Replacement/inserted content. Required for replace/insert operations.")
645
660
  },
646
661
  async execute(args, context) {
647
- const absPath = (0, import_path2.isAbsolute)(args.path) ? args.path : (0, import_path2.resolve)(context.directory, args.path);
662
+ const { path, operation, startRef, endRef, replacement } = args;
663
+ const absPath = (0, import_path2.isAbsolute)(path) ? path : (0, import_path2.resolve)(context.directory, path);
648
664
  const normalizedAbs = (0, import_path2.resolve)(absPath);
649
665
  const normalizedWorktree = (0, import_path2.resolve)(context.worktree);
650
666
  if (normalizedAbs !== normalizedWorktree && !normalizedAbs.startsWith(normalizedWorktree + import_path2.sep)) {
651
- throw new Error(`Access denied: "${args.path}" resolves outside the project directory`);
667
+ throw new Error(`Access denied: "${path}" resolves outside the project directory`);
652
668
  }
653
- const displayPath = (0, import_path2.relative)(context.worktree, absPath) || args.path;
669
+ const displayPath = (0, import_path2.relative)(context.worktree, absPath) || path;
654
670
  let current;
655
671
  try {
656
672
  current = (0, import_fs2.readFileSync)(absPath, "utf-8");
@@ -664,10 +680,10 @@ function createHashlineEditTool(config, cache) {
664
680
  try {
665
681
  const result = applyHashEdit(
666
682
  {
667
- operation: args.operation,
668
- startRef: args.startRef,
669
- endRef: args.endRef,
670
- replacement: args.replacement
683
+ operation,
684
+ startRef,
685
+ endRef,
686
+ replacement
671
687
  },
672
688
  current,
673
689
  config.hashLength || void 0
@@ -688,25 +704,25 @@ function createHashlineEditTool(config, cache) {
688
704
  if (cache) {
689
705
  cache.invalidate(absPath);
690
706
  cache.invalidate(normalizedAbs);
691
- if (args.path !== absPath) cache.invalidate(args.path);
707
+ if (path !== absPath) cache.invalidate(path);
692
708
  if (displayPath !== absPath) cache.invalidate(displayPath);
693
709
  }
694
710
  context.metadata({
695
- title: `hashline_edit: ${args.operation} ${displayPath}`,
711
+ title: `hashline_edit: ${operation} ${displayPath}`,
696
712
  metadata: {
697
713
  path: displayPath,
698
- operation: args.operation,
714
+ operation,
699
715
  startLine,
700
716
  endLine
701
717
  }
702
718
  });
703
719
  return [
704
- `Applied ${args.operation} to ${displayPath}.`,
720
+ `Applied ${operation} to ${displayPath}.`,
705
721
  `Resolved range: ${startLine}-${endLine}.`,
706
722
  "Re-read the file to get fresh hash references before the next edit."
707
723
  ].join("\n");
708
724
  }
709
- });
725
+ };
710
726
  }
711
727
 
712
728
  // src/index.ts
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  createFileEditBeforeHook,
3
3
  createFileReadAfterHook,
4
4
  createSystemPromptHook
5
- } from "./chunk-C2EVIAGV.js";
5
+ } from "./chunk-VW5NAHEY.js";
6
6
  import {
7
7
  HashlineCache,
8
8
  applyHashEdit,
@@ -18,25 +18,26 @@ import { fileURLToPath } from "url";
18
18
  // src/hashline-tool.ts
19
19
  import { readFileSync, writeFileSync } from "fs";
20
20
  import { isAbsolute, relative, resolve, sep } from "path";
21
- import { tool } from "@opencode-ai/plugin/tool";
21
+ import { z } from "zod";
22
22
  function createHashlineEditTool(config, cache) {
23
- return tool({
23
+ return {
24
24
  description: "Edit files using hashline references. Resolves refs like 5:a3f or '#HL 5:a3f|...' and applies replace/delete/insert without old_string matching.",
25
25
  args: {
26
- path: tool.schema.string().describe("Path to the file (absolute or relative to project directory)"),
27
- operation: tool.schema.enum(["replace", "delete", "insert_before", "insert_after"]).describe("Edit operation"),
28
- startRef: tool.schema.string().describe('Start hash reference, e.g. "5:a3f" or "#HL 5:a3f|const x = 1;"'),
29
- endRef: tool.schema.string().optional().describe("End hash reference for range operations. Defaults to startRef when omitted."),
30
- replacement: tool.schema.string().optional().describe("Replacement/inserted content. Required for replace/insert operations.")
26
+ path: z.string().describe("Path to the file (absolute or relative to project directory)"),
27
+ operation: z.enum(["replace", "delete", "insert_before", "insert_after"]).describe("Edit operation"),
28
+ startRef: z.string().describe('Start hash reference, e.g. "5:a3f" or "#HL 5:a3f|const x = 1;"'),
29
+ endRef: z.string().optional().describe("End hash reference for range operations. Defaults to startRef when omitted."),
30
+ replacement: z.string().optional().describe("Replacement/inserted content. Required for replace/insert operations.")
31
31
  },
32
32
  async execute(args, context) {
33
- const absPath = isAbsolute(args.path) ? args.path : resolve(context.directory, args.path);
33
+ const { path, operation, startRef, endRef, replacement } = args;
34
+ const absPath = isAbsolute(path) ? path : resolve(context.directory, path);
34
35
  const normalizedAbs = resolve(absPath);
35
36
  const normalizedWorktree = resolve(context.worktree);
36
37
  if (normalizedAbs !== normalizedWorktree && !normalizedAbs.startsWith(normalizedWorktree + sep)) {
37
- throw new Error(`Access denied: "${args.path}" resolves outside the project directory`);
38
+ throw new Error(`Access denied: "${path}" resolves outside the project directory`);
38
39
  }
39
- const displayPath = relative(context.worktree, absPath) || args.path;
40
+ const displayPath = relative(context.worktree, absPath) || path;
40
41
  let current;
41
42
  try {
42
43
  current = readFileSync(absPath, "utf-8");
@@ -50,10 +51,10 @@ function createHashlineEditTool(config, cache) {
50
51
  try {
51
52
  const result = applyHashEdit(
52
53
  {
53
- operation: args.operation,
54
- startRef: args.startRef,
55
- endRef: args.endRef,
56
- replacement: args.replacement
54
+ operation,
55
+ startRef,
56
+ endRef,
57
+ replacement
57
58
  },
58
59
  current,
59
60
  config.hashLength || void 0
@@ -74,25 +75,25 @@ function createHashlineEditTool(config, cache) {
74
75
  if (cache) {
75
76
  cache.invalidate(absPath);
76
77
  cache.invalidate(normalizedAbs);
77
- if (args.path !== absPath) cache.invalidate(args.path);
78
+ if (path !== absPath) cache.invalidate(path);
78
79
  if (displayPath !== absPath) cache.invalidate(displayPath);
79
80
  }
80
81
  context.metadata({
81
- title: `hashline_edit: ${args.operation} ${displayPath}`,
82
+ title: `hashline_edit: ${operation} ${displayPath}`,
82
83
  metadata: {
83
84
  path: displayPath,
84
- operation: args.operation,
85
+ operation,
85
86
  startLine,
86
87
  endLine
87
88
  }
88
89
  });
89
90
  return [
90
- `Applied ${args.operation} to ${displayPath}.`,
91
+ `Applied ${operation} to ${displayPath}.`,
91
92
  `Resolved range: ${startLine}-${endLine}.`,
92
93
  "Re-read the file to get fresh hash references before the next edit."
93
94
  ].join("\n");
94
95
  }
95
- });
96
+ };
96
97
  }
97
98
 
98
99
  // src/index.ts
package/dist/utils.cjs CHANGED
@@ -460,7 +460,7 @@ function debug(...args) {
460
460
  }
461
461
  }
462
462
  var FILE_READ_TOOLS = ["read", "file_read", "read_file", "cat", "view"];
463
- var FILE_EDIT_TOOLS = ["write", "file_write", "file_edit", "edit", "edit_file", "patch", "apply_patch", "multiedit"];
463
+ var FILE_EDIT_TOOLS = ["write", "file_write", "file_edit", "edit", "edit_file", "patch", "apply_patch", "multiedit", "batch"];
464
464
  function isFileReadTool(toolName, args) {
465
465
  const lower = toolName.toLowerCase();
466
466
  const nameMatch = FILE_READ_TOOLS.some(
@@ -480,8 +480,16 @@ function createFileReadAfterHook(cache, config) {
480
480
  const resolved = config ?? resolveConfig();
481
481
  const hashLen = resolved.hashLength || 0;
482
482
  const prefix = resolved.prefix;
483
+ const processedCallIds = /* @__PURE__ */ new Set();
483
484
  return async (input, output) => {
484
485
  debug("tool.execute.after:", input.tool, "args:", input.args);
486
+ if (input.callID) {
487
+ if (processedCallIds.has(input.callID)) {
488
+ debug("skipped: duplicate callID", input.callID);
489
+ return;
490
+ }
491
+ processedCallIds.add(input.callID);
492
+ }
485
493
  if (!isFileReadTool(input.tool, input.args)) {
486
494
  debug("skipped: not a file-read tool");
487
495
  return;
@@ -519,7 +527,14 @@ function createFileReadAfterHook(cache, config) {
519
527
  function createFileEditBeforeHook(config) {
520
528
  const resolved = config ?? resolveConfig();
521
529
  const prefix = resolved.prefix;
530
+ const processedCallIds = /* @__PURE__ */ new Set();
522
531
  return async (input, output) => {
532
+ if (input.callID) {
533
+ if (processedCallIds.has(input.callID)) {
534
+ return;
535
+ }
536
+ processedCallIds.add(input.callID);
537
+ }
523
538
  const toolName = input.tool.toLowerCase();
524
539
  const isFileEdit = FILE_EDIT_TOOLS.some(
525
540
  (name) => toolName === name || toolName.endsWith(`.${name}`)
package/dist/utils.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createFileReadAfterHook,
4
4
  createSystemPromptHook,
5
5
  isFileReadTool
6
- } from "./chunk-C2EVIAGV.js";
6
+ } from "./chunk-VW5NAHEY.js";
7
7
  import {
8
8
  DEFAULT_CONFIG,
9
9
  DEFAULT_EXCLUDE_PATTERNS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-hashline",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Hashline plugin for OpenCode — content-addressable line hashing for precise AI code editing",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -40,17 +40,44 @@
40
40
  ],
41
41
  "license": "MIT",
42
42
  "peerDependencies": {
43
- "@opencode-ai/plugin": "^1.2.2"
43
+ "@opencode-ai/plugin": "^1.2.2",
44
+ "zod": "^3.0.0"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@opencode-ai/plugin": "^1.2.2",
48
+ "@semantic-release/changelog": "^6.0.3",
49
+ "@semantic-release/git": "^10.0.1",
47
50
  "@types/node": "^25.2.3",
48
51
  "@types/picomatch": "^4.0.2",
52
+ "semantic-release": "^25.0.3",
49
53
  "tsup": "^8.5.1",
50
54
  "typescript": "^5.9.3",
51
- "vitest": "^4.0.18"
55
+ "vitest": "^4.0.18",
56
+ "zod": "^4.1.8"
52
57
  },
53
58
  "dependencies": {
54
59
  "picomatch": "^4.0.3"
60
+ },
61
+ "release": {
62
+ "branches": [
63
+ "main"
64
+ ],
65
+ "plugins": [
66
+ "@semantic-release/commit-analyzer",
67
+ "@semantic-release/release-notes-generator",
68
+ "@semantic-release/changelog",
69
+ "@semantic-release/npm",
70
+ "@semantic-release/github",
71
+ [
72
+ "@semantic-release/git",
73
+ {
74
+ "assets": [
75
+ "package.json",
76
+ "CHANGELOG.md"
77
+ ],
78
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
79
+ }
80
+ ]
81
+ ]
55
82
  }
56
83
  }