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 +4 -0
- package/README.ru.md +4 -0
- package/dist/{chunk-C2EVIAGV.js → chunk-VW5NAHEY.js} +16 -1
- package/dist/index.cjs +36 -20
- package/dist/index.js +21 -20
- package/dist/utils.cjs +16 -1
- package/dist/utils.js +1 -1
- package/package.json +30 -3
package/README.md
CHANGED
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
**Content-addressable line hashing for precise AI code editing**
|
|
6
6
|
|
|
7
7
|
[](https://github.com/izzzzzi/opencode-hashline/actions/workflows/ci.yml)
|
|
8
|
+
[](https://github.com/izzzzzi/opencode-hashline/actions/workflows/release.yml)
|
|
8
9
|
[](https://www.npmjs.com/package/opencode-hashline)
|
|
10
|
+
[](https://www.npmjs.com/package/opencode-hashline)
|
|
11
|
+
[](https://github.com/izzzzzi/opencode-hashline/releases)
|
|
9
12
|
[](LICENSE)
|
|
13
|
+
[](https://github.com/semantic-release/semantic-release)
|
|
10
14
|
[](https://www.typescriptlang.org/)
|
|
11
15
|
[](https://nodejs.org/)
|
|
12
16
|
|
package/README.ru.md
CHANGED
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
**Контентно-адресуемое хеширование строк для точного редактирования кода с помощью AI**
|
|
6
6
|
|
|
7
7
|
[](https://github.com/izzzzzi/opencode-hashline/actions/workflows/ci.yml)
|
|
8
|
+
[](https://github.com/izzzzzi/opencode-hashline/actions/workflows/release.yml)
|
|
8
9
|
[](https://www.npmjs.com/package/opencode-hashline)
|
|
10
|
+
[](https://www.npmjs.com/package/opencode-hashline)
|
|
11
|
+
[](https://github.com/izzzzzi/opencode-hashline/releases)
|
|
9
12
|
[](LICENSE)
|
|
13
|
+
[](https://github.com/semantic-release/semantic-release)
|
|
10
14
|
[](https://www.typescriptlang.org/)
|
|
11
15
|
[](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
|
|
649
|
+
var import_zod = require("zod");
|
|
635
650
|
init_hashline();
|
|
636
651
|
function createHashlineEditTool(config, cache) {
|
|
637
|
-
return
|
|
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:
|
|
641
|
-
operation:
|
|
642
|
-
startRef:
|
|
643
|
-
endRef:
|
|
644
|
-
replacement:
|
|
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
|
|
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: "${
|
|
667
|
+
throw new Error(`Access denied: "${path}" resolves outside the project directory`);
|
|
652
668
|
}
|
|
653
|
-
const displayPath = (0, import_path2.relative)(context.worktree, absPath) ||
|
|
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
|
|
668
|
-
startRef
|
|
669
|
-
endRef
|
|
670
|
-
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 (
|
|
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: ${
|
|
711
|
+
title: `hashline_edit: ${operation} ${displayPath}`,
|
|
696
712
|
metadata: {
|
|
697
713
|
path: displayPath,
|
|
698
|
-
operation
|
|
714
|
+
operation,
|
|
699
715
|
startLine,
|
|
700
716
|
endLine
|
|
701
717
|
}
|
|
702
718
|
});
|
|
703
719
|
return [
|
|
704
|
-
`Applied ${
|
|
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-
|
|
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 {
|
|
21
|
+
import { z } from "zod";
|
|
22
22
|
function createHashlineEditTool(config, cache) {
|
|
23
|
-
return
|
|
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:
|
|
27
|
-
operation:
|
|
28
|
-
startRef:
|
|
29
|
-
endRef:
|
|
30
|
-
replacement:
|
|
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
|
|
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: "${
|
|
38
|
+
throw new Error(`Access denied: "${path}" resolves outside the project directory`);
|
|
38
39
|
}
|
|
39
|
-
const displayPath = relative(context.worktree, absPath) ||
|
|
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
|
|
54
|
-
startRef
|
|
55
|
-
endRef
|
|
56
|
-
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 (
|
|
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: ${
|
|
82
|
+
title: `hashline_edit: ${operation} ${displayPath}`,
|
|
82
83
|
metadata: {
|
|
83
84
|
path: displayPath,
|
|
84
|
-
operation
|
|
85
|
+
operation,
|
|
85
86
|
startLine,
|
|
86
87
|
endLine
|
|
87
88
|
}
|
|
88
89
|
});
|
|
89
90
|
return [
|
|
90
|
-
`Applied ${
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-hashline",
|
|
3
|
-
"version": "1.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
|
}
|