opencode-hashline 1.0.2 → 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.
@@ -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
  `;
@@ -40,7 +53,7 @@ function createFileReadAfterHook(cache, config) {
40
53
  const resolved = config ?? resolveConfig();
41
54
  const hashLen = resolved.hashLength || 0;
42
55
  const prefix = resolved.prefix;
43
- const processedCallIds = /* @__PURE__ */ new Set();
56
+ const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
44
57
  return async (input, output) => {
45
58
  debug("tool.execute.after:", input.tool, "args:", input.args);
46
59
  if (input.callID) {
@@ -87,7 +100,7 @@ function createFileReadAfterHook(cache, config) {
87
100
  function createFileEditBeforeHook(config) {
88
101
  const resolved = config ?? resolveConfig();
89
102
  const prefix = resolved.prefix;
90
- const processedCallIds = /* @__PURE__ */ new Set();
103
+ const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
91
104
  return async (input, output) => {
92
105
  if (input.callID) {
93
106
  if (processedCallIds.has(input.callID)) {
@@ -101,7 +114,7 @@ function createFileEditBeforeHook(config) {
101
114
  );
102
115
  if (!isFileEdit) return;
103
116
  if (!output.args || typeof output.args !== "object") return;
104
- const contentFields = [
117
+ const contentFields = /* @__PURE__ */ new Set([
105
118
  "content",
106
119
  "new_content",
107
120
  "old_content",
@@ -111,13 +124,26 @@ function createFileEditBeforeHook(config) {
111
124
  "text",
112
125
  "diff",
113
126
  "patch",
114
- "patchText"
115
- ];
116
- for (const field of contentFields) {
117
- if (typeof output.args[field] === "string") {
118
- output.args[field] = stripHashes(output.args[field], prefix);
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
+ }
119
144
  }
120
145
  }
146
+ stripDeep(output.args);
121
147
  };
122
148
  }
123
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
  `;
@@ -496,7 +509,7 @@ function createFileReadAfterHook(cache, config) {
496
509
  const resolved = config ?? resolveConfig();
497
510
  const hashLen = resolved.hashLength || 0;
498
511
  const prefix = resolved.prefix;
499
- const processedCallIds = /* @__PURE__ */ new Set();
512
+ const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
500
513
  return async (input, output) => {
501
514
  debug("tool.execute.after:", input.tool, "args:", input.args);
502
515
  if (input.callID) {
@@ -543,7 +556,7 @@ function createFileReadAfterHook(cache, config) {
543
556
  function createFileEditBeforeHook(config) {
544
557
  const resolved = config ?? resolveConfig();
545
558
  const prefix = resolved.prefix;
546
- const processedCallIds = /* @__PURE__ */ new Set();
559
+ const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
547
560
  return async (input, output) => {
548
561
  if (input.callID) {
549
562
  if (processedCallIds.has(input.callID)) {
@@ -557,7 +570,7 @@ function createFileEditBeforeHook(config) {
557
570
  );
558
571
  if (!isFileEdit) return;
559
572
  if (!output.args || typeof output.args !== "object") return;
560
- const contentFields = [
573
+ const contentFields = /* @__PURE__ */ new Set([
561
574
  "content",
562
575
  "new_content",
563
576
  "old_content",
@@ -567,13 +580,26 @@ function createFileEditBeforeHook(config) {
567
580
  "text",
568
581
  "diff",
569
582
  "patch",
570
- "patchText"
571
- ];
572
- for (const field of contentFields) {
573
- if (typeof output.args[field] === "string") {
574
- output.args[field] = stripHashes(output.args[field], prefix);
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
+ }
575
600
  }
576
601
  }
602
+ stripDeep(output.args);
577
603
  };
578
604
  }
579
605
  function createSystemPromptHook(config) {
@@ -661,15 +687,21 @@ function createHashlineEditTool(config, cache) {
661
687
  async execute(args, context) {
662
688
  const { path, operation, startRef, endRef, replacement } = args;
663
689
  const absPath = (0, import_path2.isAbsolute)(path) ? path : (0, import_path2.resolve)(context.directory, path);
664
- const normalizedAbs = (0, import_path2.resolve)(absPath);
665
- const normalizedWorktree = (0, import_path2.resolve)(context.worktree);
666
- if (normalizedAbs !== normalizedWorktree && !normalizedAbs.startsWith(normalizedWorktree + import_path2.sep)) {
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)) {
667
698
  throw new Error(`Access denied: "${path}" resolves outside the project directory`);
668
699
  }
700
+ const normalizedAbs = (0, import_path2.resolve)(absPath);
669
701
  const displayPath = (0, import_path2.relative)(context.worktree, absPath) || path;
670
702
  let current;
671
703
  try {
672
- current = (0, import_fs2.readFileSync)(absPath, "utf-8");
704
+ current = (0, import_fs2.readFileSync)(realAbs, "utf-8");
673
705
  } catch (error) {
674
706
  const reason = error instanceof Error ? error.message : String(error);
675
707
  throw new Error(`Failed to read "${displayPath}": ${reason}`);
@@ -696,14 +728,15 @@ function createHashlineEditTool(config, cache) {
696
728
  throw new Error(`Hashline edit failed for "${displayPath}": ${reason}`);
697
729
  }
698
730
  try {
699
- (0, import_fs2.writeFileSync)(absPath, nextContent, "utf-8");
731
+ (0, import_fs2.writeFileSync)(realAbs, nextContent, "utf-8");
700
732
  } catch (error) {
701
733
  const reason = error instanceof Error ? error.message : String(error);
702
734
  throw new Error(`Failed to write "${displayPath}": ${reason}`);
703
735
  }
704
736
  if (cache) {
705
- cache.invalidate(absPath);
737
+ cache.invalidate(realAbs);
706
738
  cache.invalidate(normalizedAbs);
739
+ cache.invalidate(absPath);
707
740
  if (path !== absPath) cache.invalidate(path);
708
741
  if (displayPath !== absPath) cache.invalidate(displayPath);
709
742
  }
@@ -751,6 +784,7 @@ function loadConfig(projectDir, userConfig) {
751
784
  function createHashlinePlugin(userConfig) {
752
785
  return async (input) => {
753
786
  const projectDir = input.directory;
787
+ const worktree = input.worktree;
754
788
  const fileConfig = loadConfig(projectDir, userConfig);
755
789
  const config = resolveConfig(fileConfig);
756
790
  const cache = new HashlineCache(config.cacheSize);
@@ -761,6 +795,17 @@ function createHashlinePlugin(userConfig) {
761
795
  `);
762
796
  } catch {
763
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);
764
809
  return {
765
810
  tool: {
766
811
  hashline_edit: createHashlineEditTool(config, cache)
@@ -782,6 +827,17 @@ function createHashlinePlugin(userConfig) {
782
827
  filePath = (0, import_url.fileURLToPath)(p.url);
783
828
  }
784
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
+ }
785
841
  if (shouldExclude2(filePath, config.exclude)) continue;
786
842
  let content;
787
843
  try {
@@ -794,6 +850,7 @@ function createHashlinePlugin(userConfig) {
794
850
  if (cached) {
795
851
  const tmpPath2 = (0, import_path3.join)((0, import_os2.tmpdir)(), `hashline-${p.id}.txt`);
796
852
  (0, import_fs3.writeFileSync)(tmpPath2, cached, "utf-8");
853
+ tempFiles.add(tmpPath2);
797
854
  p.url = `file://${tmpPath2}`;
798
855
  writeLog(debugLog, `[${(/* @__PURE__ */ new Date()).toISOString()}] chat.message annotated (cached): ${filePath}
799
856
  `);
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  createFileEditBeforeHook,
3
3
  createFileReadAfterHook,
4
4
  createSystemPromptHook
5
- } from "./chunk-VW5NAHEY.js";
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
- const normalizedAbs = resolve(absPath);
36
- const normalizedWorktree = resolve(context.worktree);
37
- if (normalizedAbs !== normalizedWorktree && !normalizedAbs.startsWith(normalizedWorktree + sep)) {
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(absPath, "utf-8");
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(absPath, nextContent, "utf-8");
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(absPath);
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
  `;
@@ -480,7 +493,7 @@ function createFileReadAfterHook(cache, config) {
480
493
  const resolved = config ?? resolveConfig();
481
494
  const hashLen = resolved.hashLength || 0;
482
495
  const prefix = resolved.prefix;
483
- const processedCallIds = /* @__PURE__ */ new Set();
496
+ const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
484
497
  return async (input, output) => {
485
498
  debug("tool.execute.after:", input.tool, "args:", input.args);
486
499
  if (input.callID) {
@@ -527,7 +540,7 @@ function createFileReadAfterHook(cache, config) {
527
540
  function createFileEditBeforeHook(config) {
528
541
  const resolved = config ?? resolveConfig();
529
542
  const prefix = resolved.prefix;
530
- const processedCallIds = /* @__PURE__ */ new Set();
543
+ const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
531
544
  return async (input, output) => {
532
545
  if (input.callID) {
533
546
  if (processedCallIds.has(input.callID)) {
@@ -541,7 +554,7 @@ function createFileEditBeforeHook(config) {
541
554
  );
542
555
  if (!isFileEdit) return;
543
556
  if (!output.args || typeof output.args !== "object") return;
544
- const contentFields = [
557
+ const contentFields = /* @__PURE__ */ new Set([
545
558
  "content",
546
559
  "new_content",
547
560
  "old_content",
@@ -551,13 +564,26 @@ function createFileEditBeforeHook(config) {
551
564
  "text",
552
565
  "diff",
553
566
  "patch",
554
- "patchText"
555
- ];
556
- for (const field of contentFields) {
557
- if (typeof output.args[field] === "string") {
558
- output.args[field] = stripHashes(output.args[field], prefix);
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
+ }
559
584
  }
560
585
  }
586
+ stripDeep(output.args);
561
587
  };
562
588
  }
563
589
  function createSystemPromptHook(config) {
package/dist/utils.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createFileReadAfterHook,
4
4
  createSystemPromptHook,
5
5
  isFileReadTool
6
- } from "./chunk-VW5NAHEY.js";
6
+ } from "./chunk-OJ2CHKQK.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.2",
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",