opencode-hashline 1.0.0
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/LICENSE +21 -0
- package/README.md +442 -0
- package/README.ru.md +417 -0
- package/dist/chunk-C2EVIAGV.js +177 -0
- package/dist/chunk-IVZSANZ4.js +411 -0
- package/dist/hashline-Civwirvf.d.cts +278 -0
- package/dist/hashline-Civwirvf.d.ts +278 -0
- package/dist/hashline-W2FT5QN4.js +44 -0
- package/dist/index.cjs +811 -0
- package/dist/index.d.cts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +197 -0
- package/dist/utils.cjs +637 -0
- package/dist/utils.d.cts +74 -0
- package/dist/utils.d.ts +74 -0
- package/dist/utils.js +54 -0
- package/package.json +56 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Plugin } from '@opencode-ai/plugin';
|
|
2
|
+
import { H as HashlineConfig } from './hashline-Civwirvf.cjs';
|
|
3
|
+
export { a as HashEditInput, b as HashEditOperation, c as HashEditResult, d as HashlineInstance, R as ResolvedRange, V as VerifyHashResult } from './hashline-Civwirvf.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* opencode-hashline — Hashline plugin for OpenCode
|
|
7
|
+
*
|
|
8
|
+
* Content-addressable line hashing for precise AI code editing.
|
|
9
|
+
* When the AI reads a file, each line is annotated with a short hash tag.
|
|
10
|
+
* When the AI edits a file, hash prefixes are automatically stripped.
|
|
11
|
+
*
|
|
12
|
+
* IMPORTANT: OpenCode's plugin loader calls every export as a Plugin function.
|
|
13
|
+
* Only Plugin-compatible exports belong here. For utility functions and
|
|
14
|
+
* constants, import from "opencode-hashline/utils".
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a Hashline plugin instance with optional user configuration.
|
|
19
|
+
*
|
|
20
|
+
* Config is loaded from (in priority order):
|
|
21
|
+
* 1. ~/.config/opencode/opencode-hashline.json (global)
|
|
22
|
+
* 2. <project>/opencode-hashline.json (project-local)
|
|
23
|
+
* 3. Programmatic config passed to this factory
|
|
24
|
+
*
|
|
25
|
+
* Usage in opencode.json (default config):
|
|
26
|
+
* ```json
|
|
27
|
+
* { "plugin": ["opencode-hashline"] }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* For custom config, use the factory:
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { createHashlinePlugin } from "opencode-hashline";
|
|
33
|
+
* export default createHashlinePlugin({ maxFileSize: 2_000_000 });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @param userConfig - optional Hashline configuration overrides
|
|
37
|
+
* @returns an OpenCode Plugin function
|
|
38
|
+
*/
|
|
39
|
+
declare function createHashlinePlugin(userConfig?: HashlineConfig): Plugin;
|
|
40
|
+
/**
|
|
41
|
+
* Hashline plugin for OpenCode (default instance with default config).
|
|
42
|
+
*
|
|
43
|
+
* Named export following the OpenCode plugin convention:
|
|
44
|
+
* @see https://opencode.ai/docs/plugins/
|
|
45
|
+
*/
|
|
46
|
+
declare const HashlinePlugin: Plugin;
|
|
47
|
+
|
|
48
|
+
export { HashlineConfig, HashlinePlugin, createHashlinePlugin, HashlinePlugin as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Plugin } from '@opencode-ai/plugin';
|
|
2
|
+
import { H as HashlineConfig } from './hashline-Civwirvf.js';
|
|
3
|
+
export { a as HashEditInput, b as HashEditOperation, c as HashEditResult, d as HashlineInstance, R as ResolvedRange, V as VerifyHashResult } from './hashline-Civwirvf.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* opencode-hashline — Hashline plugin for OpenCode
|
|
7
|
+
*
|
|
8
|
+
* Content-addressable line hashing for precise AI code editing.
|
|
9
|
+
* When the AI reads a file, each line is annotated with a short hash tag.
|
|
10
|
+
* When the AI edits a file, hash prefixes are automatically stripped.
|
|
11
|
+
*
|
|
12
|
+
* IMPORTANT: OpenCode's plugin loader calls every export as a Plugin function.
|
|
13
|
+
* Only Plugin-compatible exports belong here. For utility functions and
|
|
14
|
+
* constants, import from "opencode-hashline/utils".
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a Hashline plugin instance with optional user configuration.
|
|
19
|
+
*
|
|
20
|
+
* Config is loaded from (in priority order):
|
|
21
|
+
* 1. ~/.config/opencode/opencode-hashline.json (global)
|
|
22
|
+
* 2. <project>/opencode-hashline.json (project-local)
|
|
23
|
+
* 3. Programmatic config passed to this factory
|
|
24
|
+
*
|
|
25
|
+
* Usage in opencode.json (default config):
|
|
26
|
+
* ```json
|
|
27
|
+
* { "plugin": ["opencode-hashline"] }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* For custom config, use the factory:
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { createHashlinePlugin } from "opencode-hashline";
|
|
33
|
+
* export default createHashlinePlugin({ maxFileSize: 2_000_000 });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @param userConfig - optional Hashline configuration overrides
|
|
37
|
+
* @returns an OpenCode Plugin function
|
|
38
|
+
*/
|
|
39
|
+
declare function createHashlinePlugin(userConfig?: HashlineConfig): Plugin;
|
|
40
|
+
/**
|
|
41
|
+
* Hashline plugin for OpenCode (default instance with default config).
|
|
42
|
+
*
|
|
43
|
+
* Named export following the OpenCode plugin convention:
|
|
44
|
+
* @see https://opencode.ai/docs/plugins/
|
|
45
|
+
*/
|
|
46
|
+
declare const HashlinePlugin: Plugin;
|
|
47
|
+
|
|
48
|
+
export { HashlineConfig, HashlinePlugin, createHashlinePlugin, HashlinePlugin as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFileEditBeforeHook,
|
|
3
|
+
createFileReadAfterHook,
|
|
4
|
+
createSystemPromptHook
|
|
5
|
+
} from "./chunk-C2EVIAGV.js";
|
|
6
|
+
import {
|
|
7
|
+
HashlineCache,
|
|
8
|
+
applyHashEdit,
|
|
9
|
+
resolveConfig
|
|
10
|
+
} from "./chunk-IVZSANZ4.js";
|
|
11
|
+
|
|
12
|
+
// src/index.ts
|
|
13
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { homedir, tmpdir } from "os";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
|
|
18
|
+
// src/hashline-tool.ts
|
|
19
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
20
|
+
import { isAbsolute, relative, resolve, sep } from "path";
|
|
21
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
22
|
+
function createHashlineEditTool(config, cache) {
|
|
23
|
+
return tool({
|
|
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
|
+
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.")
|
|
31
|
+
},
|
|
32
|
+
async execute(args, context) {
|
|
33
|
+
const absPath = isAbsolute(args.path) ? args.path : resolve(context.directory, args.path);
|
|
34
|
+
const normalizedAbs = resolve(absPath);
|
|
35
|
+
const normalizedWorktree = resolve(context.worktree);
|
|
36
|
+
if (normalizedAbs !== normalizedWorktree && !normalizedAbs.startsWith(normalizedWorktree + sep)) {
|
|
37
|
+
throw new Error(`Access denied: "${args.path}" resolves outside the project directory`);
|
|
38
|
+
}
|
|
39
|
+
const displayPath = relative(context.worktree, absPath) || args.path;
|
|
40
|
+
let current;
|
|
41
|
+
try {
|
|
42
|
+
current = readFileSync(absPath, "utf-8");
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
45
|
+
throw new Error(`Failed to read "${displayPath}": ${reason}`);
|
|
46
|
+
}
|
|
47
|
+
let nextContent;
|
|
48
|
+
let startLine;
|
|
49
|
+
let endLine;
|
|
50
|
+
try {
|
|
51
|
+
const result = applyHashEdit(
|
|
52
|
+
{
|
|
53
|
+
operation: args.operation,
|
|
54
|
+
startRef: args.startRef,
|
|
55
|
+
endRef: args.endRef,
|
|
56
|
+
replacement: args.replacement
|
|
57
|
+
},
|
|
58
|
+
current,
|
|
59
|
+
config.hashLength || void 0
|
|
60
|
+
);
|
|
61
|
+
nextContent = result.content;
|
|
62
|
+
startLine = result.startLine;
|
|
63
|
+
endLine = result.endLine;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
66
|
+
throw new Error(`Hashline edit failed for "${displayPath}": ${reason}`);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
writeFileSync(absPath, nextContent, "utf-8");
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
72
|
+
throw new Error(`Failed to write "${displayPath}": ${reason}`);
|
|
73
|
+
}
|
|
74
|
+
if (cache) {
|
|
75
|
+
cache.invalidate(absPath);
|
|
76
|
+
cache.invalidate(normalizedAbs);
|
|
77
|
+
if (args.path !== absPath) cache.invalidate(args.path);
|
|
78
|
+
if (displayPath !== absPath) cache.invalidate(displayPath);
|
|
79
|
+
}
|
|
80
|
+
context.metadata({
|
|
81
|
+
title: `hashline_edit: ${args.operation} ${displayPath}`,
|
|
82
|
+
metadata: {
|
|
83
|
+
path: displayPath,
|
|
84
|
+
operation: args.operation,
|
|
85
|
+
startLine,
|
|
86
|
+
endLine
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return [
|
|
90
|
+
`Applied ${args.operation} to ${displayPath}.`,
|
|
91
|
+
`Resolved range: ${startLine}-${endLine}.`,
|
|
92
|
+
"Re-read the file to get fresh hash references before the next edit."
|
|
93
|
+
].join("\n");
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/index.ts
|
|
99
|
+
var CONFIG_FILENAME = "opencode-hashline.json";
|
|
100
|
+
function loadConfigFile(filePath) {
|
|
101
|
+
try {
|
|
102
|
+
const raw = readFileSync2(filePath, "utf-8");
|
|
103
|
+
return JSON.parse(raw);
|
|
104
|
+
} catch {
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function loadConfig(projectDir, userConfig) {
|
|
109
|
+
const globalPath = join(homedir(), ".config", "opencode", CONFIG_FILENAME);
|
|
110
|
+
const globalConfig = loadConfigFile(globalPath);
|
|
111
|
+
let projectConfig;
|
|
112
|
+
if (projectDir) {
|
|
113
|
+
projectConfig = loadConfigFile(join(projectDir, CONFIG_FILENAME));
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
...globalConfig,
|
|
117
|
+
...projectConfig,
|
|
118
|
+
...userConfig
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function createHashlinePlugin(userConfig) {
|
|
122
|
+
return async (input) => {
|
|
123
|
+
const projectDir = input.directory;
|
|
124
|
+
const fileConfig = loadConfig(projectDir, userConfig);
|
|
125
|
+
const config = resolveConfig(fileConfig);
|
|
126
|
+
const cache = new HashlineCache(config.cacheSize);
|
|
127
|
+
const { appendFileSync: writeLog } = await import("fs");
|
|
128
|
+
const debugLog = join(homedir(), ".config", "opencode", "hashline-debug.log");
|
|
129
|
+
try {
|
|
130
|
+
writeLog(debugLog, `[${(/* @__PURE__ */ new Date()).toISOString()}] plugin loaded, prefix: ${JSON.stringify(config.prefix)}, maxFileSize: ${config.maxFileSize}, projectDir: ${projectDir}
|
|
131
|
+
`);
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
tool: {
|
|
136
|
+
hashline_edit: createHashlineEditTool(config, cache)
|
|
137
|
+
},
|
|
138
|
+
"tool.execute.after": createFileReadAfterHook(cache, config),
|
|
139
|
+
"tool.execute.before": createFileEditBeforeHook(config),
|
|
140
|
+
"experimental.chat.system.transform": createSystemPromptHook(config),
|
|
141
|
+
"chat.message": async (_input, output) => {
|
|
142
|
+
try {
|
|
143
|
+
const out = output;
|
|
144
|
+
const hashLen = config.hashLength || 0;
|
|
145
|
+
const prefix = config.prefix;
|
|
146
|
+
const { formatFileWithHashes, shouldExclude, getByteLength } = await import("./hashline-W2FT5QN4.js");
|
|
147
|
+
for (const p of out.parts ?? []) {
|
|
148
|
+
if (p.type !== "file") continue;
|
|
149
|
+
if (!p.url || !p.mime?.startsWith("text/")) continue;
|
|
150
|
+
let filePath;
|
|
151
|
+
if (typeof p.url === "string" && p.url.startsWith("file://")) {
|
|
152
|
+
filePath = fileURLToPath(p.url);
|
|
153
|
+
}
|
|
154
|
+
if (!filePath) continue;
|
|
155
|
+
if (shouldExclude(filePath, config.exclude)) continue;
|
|
156
|
+
let content;
|
|
157
|
+
try {
|
|
158
|
+
content = readFileSync2(filePath, "utf-8");
|
|
159
|
+
} catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (config.maxFileSize > 0 && getByteLength(content) > config.maxFileSize) continue;
|
|
163
|
+
const cached = cache.get(filePath, content);
|
|
164
|
+
if (cached) {
|
|
165
|
+
const tmpPath2 = join(tmpdir(), `hashline-${p.id}.txt`);
|
|
166
|
+
writeFileSync2(tmpPath2, cached, "utf-8");
|
|
167
|
+
p.url = `file://${tmpPath2}`;
|
|
168
|
+
writeLog(debugLog, `[${(/* @__PURE__ */ new Date()).toISOString()}] chat.message annotated (cached): ${filePath}
|
|
169
|
+
`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const annotated = formatFileWithHashes(content, hashLen || void 0, prefix);
|
|
173
|
+
cache.set(filePath, content, annotated);
|
|
174
|
+
const tmpPath = join(tmpdir(), `hashline-${p.id}.txt`);
|
|
175
|
+
writeFileSync2(tmpPath, annotated, "utf-8");
|
|
176
|
+
p.url = `file://${tmpPath}`;
|
|
177
|
+
writeLog(debugLog, `[${(/* @__PURE__ */ new Date()).toISOString()}] chat.message annotated: ${filePath} lines=${content.split("\n").length}
|
|
178
|
+
`);
|
|
179
|
+
}
|
|
180
|
+
} catch (e) {
|
|
181
|
+
try {
|
|
182
|
+
writeLog(debugLog, `[${(/* @__PURE__ */ new Date()).toISOString()}] chat.message error: ${e}
|
|
183
|
+
`);
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
var HashlinePlugin = createHashlinePlugin();
|
|
192
|
+
var index_default = HashlinePlugin;
|
|
193
|
+
export {
|
|
194
|
+
HashlinePlugin,
|
|
195
|
+
createHashlinePlugin,
|
|
196
|
+
index_default as default
|
|
197
|
+
};
|