opencode-hashline 1.0.1 → 1.0.3
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-OJ2CHKQK.js} +48 -7
- package/dist/index.cjs +85 -13
- package/dist/index.js +41 -10
- package/dist/utils.cjs +48 -7
- package/dist/utils.js +1 -1
- package/package.json +26 -1
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
|
|
|
@@ -11,6 +11,19 @@ import { appendFileSync } from "fs";
|
|
|
11
11
|
import { join } from "path";
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
var DEBUG_LOG = join(homedir(), ".config", "opencode", "hashline-debug.log");
|
|
14
|
+
var MAX_PROCESSED_IDS = 1e4;
|
|
15
|
+
function createBoundedSet(maxSize) {
|
|
16
|
+
const set = /* @__PURE__ */ new Set();
|
|
17
|
+
const originalAdd = set.add.bind(set);
|
|
18
|
+
set.add = (value) => {
|
|
19
|
+
if (set.size >= maxSize) {
|
|
20
|
+
const first = set.values().next().value;
|
|
21
|
+
if (first !== void 0) set.delete(first);
|
|
22
|
+
}
|
|
23
|
+
return originalAdd(value);
|
|
24
|
+
};
|
|
25
|
+
return set;
|
|
26
|
+
}
|
|
14
27
|
function debug(...args) {
|
|
15
28
|
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
|
|
16
29
|
`;
|
|
@@ -20,7 +33,7 @@ function debug(...args) {
|
|
|
20
33
|
}
|
|
21
34
|
}
|
|
22
35
|
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"];
|
|
36
|
+
var FILE_EDIT_TOOLS = ["write", "file_write", "file_edit", "edit", "edit_file", "patch", "apply_patch", "multiedit", "batch"];
|
|
24
37
|
function isFileReadTool(toolName, args) {
|
|
25
38
|
const lower = toolName.toLowerCase();
|
|
26
39
|
const nameMatch = FILE_READ_TOOLS.some(
|
|
@@ -40,8 +53,16 @@ function createFileReadAfterHook(cache, config) {
|
|
|
40
53
|
const resolved = config ?? resolveConfig();
|
|
41
54
|
const hashLen = resolved.hashLength || 0;
|
|
42
55
|
const prefix = resolved.prefix;
|
|
56
|
+
const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
|
|
43
57
|
return async (input, output) => {
|
|
44
58
|
debug("tool.execute.after:", input.tool, "args:", input.args);
|
|
59
|
+
if (input.callID) {
|
|
60
|
+
if (processedCallIds.has(input.callID)) {
|
|
61
|
+
debug("skipped: duplicate callID", input.callID);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
processedCallIds.add(input.callID);
|
|
65
|
+
}
|
|
45
66
|
if (!isFileReadTool(input.tool, input.args)) {
|
|
46
67
|
debug("skipped: not a file-read tool");
|
|
47
68
|
return;
|
|
@@ -79,14 +100,21 @@ function createFileReadAfterHook(cache, config) {
|
|
|
79
100
|
function createFileEditBeforeHook(config) {
|
|
80
101
|
const resolved = config ?? resolveConfig();
|
|
81
102
|
const prefix = resolved.prefix;
|
|
103
|
+
const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
|
|
82
104
|
return async (input, output) => {
|
|
105
|
+
if (input.callID) {
|
|
106
|
+
if (processedCallIds.has(input.callID)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
processedCallIds.add(input.callID);
|
|
110
|
+
}
|
|
83
111
|
const toolName = input.tool.toLowerCase();
|
|
84
112
|
const isFileEdit = FILE_EDIT_TOOLS.some(
|
|
85
113
|
(name) => toolName === name || toolName.endsWith(`.${name}`)
|
|
86
114
|
);
|
|
87
115
|
if (!isFileEdit) return;
|
|
88
116
|
if (!output.args || typeof output.args !== "object") return;
|
|
89
|
-
const contentFields = [
|
|
117
|
+
const contentFields = /* @__PURE__ */ new Set([
|
|
90
118
|
"content",
|
|
91
119
|
"new_content",
|
|
92
120
|
"old_content",
|
|
@@ -96,13 +124,26 @@ function createFileEditBeforeHook(config) {
|
|
|
96
124
|
"text",
|
|
97
125
|
"diff",
|
|
98
126
|
"patch",
|
|
99
|
-
"patchText"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
"patchText",
|
|
128
|
+
"body"
|
|
129
|
+
]);
|
|
130
|
+
function stripDeep(obj) {
|
|
131
|
+
for (const key of Object.keys(obj)) {
|
|
132
|
+
const val = obj[key];
|
|
133
|
+
if (typeof val === "string" && contentFields.has(key)) {
|
|
134
|
+
obj[key] = stripHashes(val, prefix);
|
|
135
|
+
} else if (Array.isArray(val)) {
|
|
136
|
+
for (const item of val) {
|
|
137
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
138
|
+
stripDeep(item);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
142
|
+
stripDeep(val);
|
|
143
|
+
}
|
|
104
144
|
}
|
|
105
145
|
}
|
|
146
|
+
stripDeep(output.args);
|
|
106
147
|
};
|
|
107
148
|
}
|
|
108
149
|
function createSystemPromptHook(config) {
|
package/dist/index.cjs
CHANGED
|
@@ -467,6 +467,19 @@ var import_path = require("path");
|
|
|
467
467
|
var import_os = require("os");
|
|
468
468
|
init_hashline();
|
|
469
469
|
var DEBUG_LOG = (0, import_path.join)((0, import_os.homedir)(), ".config", "opencode", "hashline-debug.log");
|
|
470
|
+
var MAX_PROCESSED_IDS = 1e4;
|
|
471
|
+
function createBoundedSet(maxSize) {
|
|
472
|
+
const set = /* @__PURE__ */ new Set();
|
|
473
|
+
const originalAdd = set.add.bind(set);
|
|
474
|
+
set.add = (value) => {
|
|
475
|
+
if (set.size >= maxSize) {
|
|
476
|
+
const first = set.values().next().value;
|
|
477
|
+
if (first !== void 0) set.delete(first);
|
|
478
|
+
}
|
|
479
|
+
return originalAdd(value);
|
|
480
|
+
};
|
|
481
|
+
return set;
|
|
482
|
+
}
|
|
470
483
|
function debug(...args) {
|
|
471
484
|
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
|
|
472
485
|
`;
|
|
@@ -476,7 +489,7 @@ function debug(...args) {
|
|
|
476
489
|
}
|
|
477
490
|
}
|
|
478
491
|
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"];
|
|
492
|
+
var FILE_EDIT_TOOLS = ["write", "file_write", "file_edit", "edit", "edit_file", "patch", "apply_patch", "multiedit", "batch"];
|
|
480
493
|
function isFileReadTool(toolName, args) {
|
|
481
494
|
const lower = toolName.toLowerCase();
|
|
482
495
|
const nameMatch = FILE_READ_TOOLS.some(
|
|
@@ -496,8 +509,16 @@ function createFileReadAfterHook(cache, config) {
|
|
|
496
509
|
const resolved = config ?? resolveConfig();
|
|
497
510
|
const hashLen = resolved.hashLength || 0;
|
|
498
511
|
const prefix = resolved.prefix;
|
|
512
|
+
const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
|
|
499
513
|
return async (input, output) => {
|
|
500
514
|
debug("tool.execute.after:", input.tool, "args:", input.args);
|
|
515
|
+
if (input.callID) {
|
|
516
|
+
if (processedCallIds.has(input.callID)) {
|
|
517
|
+
debug("skipped: duplicate callID", input.callID);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
processedCallIds.add(input.callID);
|
|
521
|
+
}
|
|
501
522
|
if (!isFileReadTool(input.tool, input.args)) {
|
|
502
523
|
debug("skipped: not a file-read tool");
|
|
503
524
|
return;
|
|
@@ -535,14 +556,21 @@ function createFileReadAfterHook(cache, config) {
|
|
|
535
556
|
function createFileEditBeforeHook(config) {
|
|
536
557
|
const resolved = config ?? resolveConfig();
|
|
537
558
|
const prefix = resolved.prefix;
|
|
559
|
+
const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
|
|
538
560
|
return async (input, output) => {
|
|
561
|
+
if (input.callID) {
|
|
562
|
+
if (processedCallIds.has(input.callID)) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
processedCallIds.add(input.callID);
|
|
566
|
+
}
|
|
539
567
|
const toolName = input.tool.toLowerCase();
|
|
540
568
|
const isFileEdit = FILE_EDIT_TOOLS.some(
|
|
541
569
|
(name) => toolName === name || toolName.endsWith(`.${name}`)
|
|
542
570
|
);
|
|
543
571
|
if (!isFileEdit) return;
|
|
544
572
|
if (!output.args || typeof output.args !== "object") return;
|
|
545
|
-
const contentFields = [
|
|
573
|
+
const contentFields = /* @__PURE__ */ new Set([
|
|
546
574
|
"content",
|
|
547
575
|
"new_content",
|
|
548
576
|
"old_content",
|
|
@@ -552,13 +580,26 @@ function createFileEditBeforeHook(config) {
|
|
|
552
580
|
"text",
|
|
553
581
|
"diff",
|
|
554
582
|
"patch",
|
|
555
|
-
"patchText"
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
583
|
+
"patchText",
|
|
584
|
+
"body"
|
|
585
|
+
]);
|
|
586
|
+
function stripDeep(obj) {
|
|
587
|
+
for (const key of Object.keys(obj)) {
|
|
588
|
+
const val = obj[key];
|
|
589
|
+
if (typeof val === "string" && contentFields.has(key)) {
|
|
590
|
+
obj[key] = stripHashes(val, prefix);
|
|
591
|
+
} else if (Array.isArray(val)) {
|
|
592
|
+
for (const item of val) {
|
|
593
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
594
|
+
stripDeep(item);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
598
|
+
stripDeep(val);
|
|
599
|
+
}
|
|
560
600
|
}
|
|
561
601
|
}
|
|
602
|
+
stripDeep(output.args);
|
|
562
603
|
};
|
|
563
604
|
}
|
|
564
605
|
function createSystemPromptHook(config) {
|
|
@@ -646,15 +687,21 @@ function createHashlineEditTool(config, cache) {
|
|
|
646
687
|
async execute(args, context) {
|
|
647
688
|
const { path, operation, startRef, endRef, replacement } = args;
|
|
648
689
|
const absPath = (0, import_path2.isAbsolute)(path) ? path : (0, import_path2.resolve)(context.directory, path);
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
690
|
+
let realAbs;
|
|
691
|
+
try {
|
|
692
|
+
realAbs = (0, import_fs2.realpathSync)(absPath);
|
|
693
|
+
} catch {
|
|
694
|
+
realAbs = (0, import_path2.resolve)(absPath);
|
|
695
|
+
}
|
|
696
|
+
const realWorktree = (0, import_fs2.realpathSync)((0, import_path2.resolve)(context.worktree));
|
|
697
|
+
if (realAbs !== realWorktree && !realAbs.startsWith(realWorktree + import_path2.sep)) {
|
|
652
698
|
throw new Error(`Access denied: "${path}" resolves outside the project directory`);
|
|
653
699
|
}
|
|
700
|
+
const normalizedAbs = (0, import_path2.resolve)(absPath);
|
|
654
701
|
const displayPath = (0, import_path2.relative)(context.worktree, absPath) || path;
|
|
655
702
|
let current;
|
|
656
703
|
try {
|
|
657
|
-
current = (0, import_fs2.readFileSync)(
|
|
704
|
+
current = (0, import_fs2.readFileSync)(realAbs, "utf-8");
|
|
658
705
|
} catch (error) {
|
|
659
706
|
const reason = error instanceof Error ? error.message : String(error);
|
|
660
707
|
throw new Error(`Failed to read "${displayPath}": ${reason}`);
|
|
@@ -681,14 +728,15 @@ function createHashlineEditTool(config, cache) {
|
|
|
681
728
|
throw new Error(`Hashline edit failed for "${displayPath}": ${reason}`);
|
|
682
729
|
}
|
|
683
730
|
try {
|
|
684
|
-
(0, import_fs2.writeFileSync)(
|
|
731
|
+
(0, import_fs2.writeFileSync)(realAbs, nextContent, "utf-8");
|
|
685
732
|
} catch (error) {
|
|
686
733
|
const reason = error instanceof Error ? error.message : String(error);
|
|
687
734
|
throw new Error(`Failed to write "${displayPath}": ${reason}`);
|
|
688
735
|
}
|
|
689
736
|
if (cache) {
|
|
690
|
-
cache.invalidate(
|
|
737
|
+
cache.invalidate(realAbs);
|
|
691
738
|
cache.invalidate(normalizedAbs);
|
|
739
|
+
cache.invalidate(absPath);
|
|
692
740
|
if (path !== absPath) cache.invalidate(path);
|
|
693
741
|
if (displayPath !== absPath) cache.invalidate(displayPath);
|
|
694
742
|
}
|
|
@@ -736,6 +784,7 @@ function loadConfig(projectDir, userConfig) {
|
|
|
736
784
|
function createHashlinePlugin(userConfig) {
|
|
737
785
|
return async (input) => {
|
|
738
786
|
const projectDir = input.directory;
|
|
787
|
+
const worktree = input.worktree;
|
|
739
788
|
const fileConfig = loadConfig(projectDir, userConfig);
|
|
740
789
|
const config = resolveConfig(fileConfig);
|
|
741
790
|
const cache = new HashlineCache(config.cacheSize);
|
|
@@ -746,6 +795,17 @@ function createHashlinePlugin(userConfig) {
|
|
|
746
795
|
`);
|
|
747
796
|
} catch {
|
|
748
797
|
}
|
|
798
|
+
const tempFiles = /* @__PURE__ */ new Set();
|
|
799
|
+
const cleanupTempFiles = () => {
|
|
800
|
+
for (const f of tempFiles) {
|
|
801
|
+
try {
|
|
802
|
+
(0, import_fs3.unlinkSync)(f);
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
tempFiles.clear();
|
|
807
|
+
};
|
|
808
|
+
process.on("exit", cleanupTempFiles);
|
|
749
809
|
return {
|
|
750
810
|
tool: {
|
|
751
811
|
hashline_edit: createHashlineEditTool(config, cache)
|
|
@@ -767,6 +827,17 @@ function createHashlinePlugin(userConfig) {
|
|
|
767
827
|
filePath = (0, import_url.fileURLToPath)(p.url);
|
|
768
828
|
}
|
|
769
829
|
if (!filePath) continue;
|
|
830
|
+
if (worktree) {
|
|
831
|
+
try {
|
|
832
|
+
const realFile = (0, import_fs3.realpathSync)(filePath);
|
|
833
|
+
const realWorktree = (0, import_fs3.realpathSync)((0, import_path3.resolve)(worktree));
|
|
834
|
+
if (realFile !== realWorktree && !realFile.startsWith(realWorktree + import_path3.sep)) {
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
} catch {
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
770
841
|
if (shouldExclude2(filePath, config.exclude)) continue;
|
|
771
842
|
let content;
|
|
772
843
|
try {
|
|
@@ -779,6 +850,7 @@ function createHashlinePlugin(userConfig) {
|
|
|
779
850
|
if (cached) {
|
|
780
851
|
const tmpPath2 = (0, import_path3.join)((0, import_os2.tmpdir)(), `hashline-${p.id}.txt`);
|
|
781
852
|
(0, import_fs3.writeFileSync)(tmpPath2, cached, "utf-8");
|
|
853
|
+
tempFiles.add(tmpPath2);
|
|
782
854
|
p.url = `file://${tmpPath2}`;
|
|
783
855
|
writeLog(debugLog, `[${(/* @__PURE__ */ new Date()).toISOString()}] chat.message annotated (cached): ${filePath}
|
|
784
856
|
`);
|
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-OJ2CHKQK.js";
|
|
6
6
|
import {
|
|
7
7
|
HashlineCache,
|
|
8
8
|
applyHashEdit,
|
|
@@ -10,13 +10,13 @@ import {
|
|
|
10
10
|
} from "./chunk-IVZSANZ4.js";
|
|
11
11
|
|
|
12
12
|
// src/index.ts
|
|
13
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
14
|
-
import { join } from "path";
|
|
13
|
+
import { readFileSync as readFileSync2, realpathSync as realpathSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
14
|
+
import { join, resolve as resolve2, sep as sep2 } from "path";
|
|
15
15
|
import { homedir, tmpdir } from "os";
|
|
16
16
|
import { fileURLToPath } from "url";
|
|
17
17
|
|
|
18
18
|
// src/hashline-tool.ts
|
|
19
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
19
|
+
import { readFileSync, realpathSync, writeFileSync } from "fs";
|
|
20
20
|
import { isAbsolute, relative, resolve, sep } from "path";
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
function createHashlineEditTool(config, cache) {
|
|
@@ -32,15 +32,21 @@ function createHashlineEditTool(config, cache) {
|
|
|
32
32
|
async execute(args, context) {
|
|
33
33
|
const { path, operation, startRef, endRef, replacement } = args;
|
|
34
34
|
const absPath = isAbsolute(path) ? path : resolve(context.directory, path);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
let realAbs;
|
|
36
|
+
try {
|
|
37
|
+
realAbs = realpathSync(absPath);
|
|
38
|
+
} catch {
|
|
39
|
+
realAbs = resolve(absPath);
|
|
40
|
+
}
|
|
41
|
+
const realWorktree = realpathSync(resolve(context.worktree));
|
|
42
|
+
if (realAbs !== realWorktree && !realAbs.startsWith(realWorktree + sep)) {
|
|
38
43
|
throw new Error(`Access denied: "${path}" resolves outside the project directory`);
|
|
39
44
|
}
|
|
45
|
+
const normalizedAbs = resolve(absPath);
|
|
40
46
|
const displayPath = relative(context.worktree, absPath) || path;
|
|
41
47
|
let current;
|
|
42
48
|
try {
|
|
43
|
-
current = readFileSync(
|
|
49
|
+
current = readFileSync(realAbs, "utf-8");
|
|
44
50
|
} catch (error) {
|
|
45
51
|
const reason = error instanceof Error ? error.message : String(error);
|
|
46
52
|
throw new Error(`Failed to read "${displayPath}": ${reason}`);
|
|
@@ -67,14 +73,15 @@ function createHashlineEditTool(config, cache) {
|
|
|
67
73
|
throw new Error(`Hashline edit failed for "${displayPath}": ${reason}`);
|
|
68
74
|
}
|
|
69
75
|
try {
|
|
70
|
-
writeFileSync(
|
|
76
|
+
writeFileSync(realAbs, nextContent, "utf-8");
|
|
71
77
|
} catch (error) {
|
|
72
78
|
const reason = error instanceof Error ? error.message : String(error);
|
|
73
79
|
throw new Error(`Failed to write "${displayPath}": ${reason}`);
|
|
74
80
|
}
|
|
75
81
|
if (cache) {
|
|
76
|
-
cache.invalidate(
|
|
82
|
+
cache.invalidate(realAbs);
|
|
77
83
|
cache.invalidate(normalizedAbs);
|
|
84
|
+
cache.invalidate(absPath);
|
|
78
85
|
if (path !== absPath) cache.invalidate(path);
|
|
79
86
|
if (displayPath !== absPath) cache.invalidate(displayPath);
|
|
80
87
|
}
|
|
@@ -122,6 +129,7 @@ function loadConfig(projectDir, userConfig) {
|
|
|
122
129
|
function createHashlinePlugin(userConfig) {
|
|
123
130
|
return async (input) => {
|
|
124
131
|
const projectDir = input.directory;
|
|
132
|
+
const worktree = input.worktree;
|
|
125
133
|
const fileConfig = loadConfig(projectDir, userConfig);
|
|
126
134
|
const config = resolveConfig(fileConfig);
|
|
127
135
|
const cache = new HashlineCache(config.cacheSize);
|
|
@@ -132,6 +140,17 @@ function createHashlinePlugin(userConfig) {
|
|
|
132
140
|
`);
|
|
133
141
|
} catch {
|
|
134
142
|
}
|
|
143
|
+
const tempFiles = /* @__PURE__ */ new Set();
|
|
144
|
+
const cleanupTempFiles = () => {
|
|
145
|
+
for (const f of tempFiles) {
|
|
146
|
+
try {
|
|
147
|
+
unlinkSync(f);
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
tempFiles.clear();
|
|
152
|
+
};
|
|
153
|
+
process.on("exit", cleanupTempFiles);
|
|
135
154
|
return {
|
|
136
155
|
tool: {
|
|
137
156
|
hashline_edit: createHashlineEditTool(config, cache)
|
|
@@ -153,6 +172,17 @@ function createHashlinePlugin(userConfig) {
|
|
|
153
172
|
filePath = fileURLToPath(p.url);
|
|
154
173
|
}
|
|
155
174
|
if (!filePath) continue;
|
|
175
|
+
if (worktree) {
|
|
176
|
+
try {
|
|
177
|
+
const realFile = realpathSync2(filePath);
|
|
178
|
+
const realWorktree = realpathSync2(resolve2(worktree));
|
|
179
|
+
if (realFile !== realWorktree && !realFile.startsWith(realWorktree + sep2)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
156
186
|
if (shouldExclude(filePath, config.exclude)) continue;
|
|
157
187
|
let content;
|
|
158
188
|
try {
|
|
@@ -165,6 +195,7 @@ function createHashlinePlugin(userConfig) {
|
|
|
165
195
|
if (cached) {
|
|
166
196
|
const tmpPath2 = join(tmpdir(), `hashline-${p.id}.txt`);
|
|
167
197
|
writeFileSync2(tmpPath2, cached, "utf-8");
|
|
198
|
+
tempFiles.add(tmpPath2);
|
|
168
199
|
p.url = `file://${tmpPath2}`;
|
|
169
200
|
writeLog(debugLog, `[${(/* @__PURE__ */ new Date()).toISOString()}] chat.message annotated (cached): ${filePath}
|
|
170
201
|
`);
|
package/dist/utils.cjs
CHANGED
|
@@ -451,6 +451,19 @@ var import_fs = require("fs");
|
|
|
451
451
|
var import_path = require("path");
|
|
452
452
|
var import_os = require("os");
|
|
453
453
|
var DEBUG_LOG = (0, import_path.join)((0, import_os.homedir)(), ".config", "opencode", "hashline-debug.log");
|
|
454
|
+
var MAX_PROCESSED_IDS = 1e4;
|
|
455
|
+
function createBoundedSet(maxSize) {
|
|
456
|
+
const set = /* @__PURE__ */ new Set();
|
|
457
|
+
const originalAdd = set.add.bind(set);
|
|
458
|
+
set.add = (value) => {
|
|
459
|
+
if (set.size >= maxSize) {
|
|
460
|
+
const first = set.values().next().value;
|
|
461
|
+
if (first !== void 0) set.delete(first);
|
|
462
|
+
}
|
|
463
|
+
return originalAdd(value);
|
|
464
|
+
};
|
|
465
|
+
return set;
|
|
466
|
+
}
|
|
454
467
|
function debug(...args) {
|
|
455
468
|
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
|
|
456
469
|
`;
|
|
@@ -460,7 +473,7 @@ function debug(...args) {
|
|
|
460
473
|
}
|
|
461
474
|
}
|
|
462
475
|
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"];
|
|
476
|
+
var FILE_EDIT_TOOLS = ["write", "file_write", "file_edit", "edit", "edit_file", "patch", "apply_patch", "multiedit", "batch"];
|
|
464
477
|
function isFileReadTool(toolName, args) {
|
|
465
478
|
const lower = toolName.toLowerCase();
|
|
466
479
|
const nameMatch = FILE_READ_TOOLS.some(
|
|
@@ -480,8 +493,16 @@ function createFileReadAfterHook(cache, config) {
|
|
|
480
493
|
const resolved = config ?? resolveConfig();
|
|
481
494
|
const hashLen = resolved.hashLength || 0;
|
|
482
495
|
const prefix = resolved.prefix;
|
|
496
|
+
const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
|
|
483
497
|
return async (input, output) => {
|
|
484
498
|
debug("tool.execute.after:", input.tool, "args:", input.args);
|
|
499
|
+
if (input.callID) {
|
|
500
|
+
if (processedCallIds.has(input.callID)) {
|
|
501
|
+
debug("skipped: duplicate callID", input.callID);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
processedCallIds.add(input.callID);
|
|
505
|
+
}
|
|
485
506
|
if (!isFileReadTool(input.tool, input.args)) {
|
|
486
507
|
debug("skipped: not a file-read tool");
|
|
487
508
|
return;
|
|
@@ -519,14 +540,21 @@ function createFileReadAfterHook(cache, config) {
|
|
|
519
540
|
function createFileEditBeforeHook(config) {
|
|
520
541
|
const resolved = config ?? resolveConfig();
|
|
521
542
|
const prefix = resolved.prefix;
|
|
543
|
+
const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
|
|
522
544
|
return async (input, output) => {
|
|
545
|
+
if (input.callID) {
|
|
546
|
+
if (processedCallIds.has(input.callID)) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
processedCallIds.add(input.callID);
|
|
550
|
+
}
|
|
523
551
|
const toolName = input.tool.toLowerCase();
|
|
524
552
|
const isFileEdit = FILE_EDIT_TOOLS.some(
|
|
525
553
|
(name) => toolName === name || toolName.endsWith(`.${name}`)
|
|
526
554
|
);
|
|
527
555
|
if (!isFileEdit) return;
|
|
528
556
|
if (!output.args || typeof output.args !== "object") return;
|
|
529
|
-
const contentFields = [
|
|
557
|
+
const contentFields = /* @__PURE__ */ new Set([
|
|
530
558
|
"content",
|
|
531
559
|
"new_content",
|
|
532
560
|
"old_content",
|
|
@@ -536,13 +564,26 @@ function createFileEditBeforeHook(config) {
|
|
|
536
564
|
"text",
|
|
537
565
|
"diff",
|
|
538
566
|
"patch",
|
|
539
|
-
"patchText"
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
567
|
+
"patchText",
|
|
568
|
+
"body"
|
|
569
|
+
]);
|
|
570
|
+
function stripDeep(obj) {
|
|
571
|
+
for (const key of Object.keys(obj)) {
|
|
572
|
+
const val = obj[key];
|
|
573
|
+
if (typeof val === "string" && contentFields.has(key)) {
|
|
574
|
+
obj[key] = stripHashes(val, prefix);
|
|
575
|
+
} else if (Array.isArray(val)) {
|
|
576
|
+
for (const item of val) {
|
|
577
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
578
|
+
stripDeep(item);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
582
|
+
stripDeep(val);
|
|
583
|
+
}
|
|
544
584
|
}
|
|
545
585
|
}
|
|
586
|
+
stripDeep(output.args);
|
|
546
587
|
};
|
|
547
588
|
}
|
|
548
589
|
function createSystemPromptHook(config) {
|
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.3",
|
|
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",
|
|
@@ -45,8 +45,11 @@
|
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@opencode-ai/plugin": "^1.2.2",
|
|
48
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
49
|
+
"@semantic-release/git": "^10.0.1",
|
|
48
50
|
"@types/node": "^25.2.3",
|
|
49
51
|
"@types/picomatch": "^4.0.2",
|
|
52
|
+
"semantic-release": "^25.0.3",
|
|
50
53
|
"tsup": "^8.5.1",
|
|
51
54
|
"typescript": "^5.9.3",
|
|
52
55
|
"vitest": "^4.0.18",
|
|
@@ -54,5 +57,27 @@
|
|
|
54
57
|
},
|
|
55
58
|
"dependencies": {
|
|
56
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
|
+
]
|
|
57
82
|
}
|
|
58
83
|
}
|