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.
@@ -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 };
@@ -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
+ };