@manishbht/helpcode 0.2.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -0
  3. package/dist/bin/helpcode.d.ts +5 -0
  4. package/dist/bin/helpcode.js +10 -0
  5. package/dist/bin/helpcode.js.map +1 -0
  6. package/dist/src/commands/apply.d.ts +15 -0
  7. package/dist/src/commands/apply.js +195 -0
  8. package/dist/src/commands/apply.js.map +1 -0
  9. package/dist/src/commands/ask.d.ts +12 -0
  10. package/dist/src/commands/ask.js +93 -0
  11. package/dist/src/commands/ask.js.map +1 -0
  12. package/dist/src/commands/init.d.ts +13 -0
  13. package/dist/src/commands/init.js +91 -0
  14. package/dist/src/commands/init.js.map +1 -0
  15. package/dist/src/commands/reset.d.ts +8 -0
  16. package/dist/src/commands/reset.js +19 -0
  17. package/dist/src/commands/reset.js.map +1 -0
  18. package/dist/src/commands/run.d.ts +15 -0
  19. package/dist/src/commands/run.js +109 -0
  20. package/dist/src/commands/run.js.map +1 -0
  21. package/dist/src/commands/status.d.ts +4 -0
  22. package/dist/src/commands/status.js +53 -0
  23. package/dist/src/commands/status.js.map +1 -0
  24. package/dist/src/core/llmSelector.d.ts +65 -0
  25. package/dist/src/core/llmSelector.js +134 -0
  26. package/dist/src/core/llmSelector.js.map +1 -0
  27. package/dist/src/core/ollama.d.ts +43 -0
  28. package/dist/src/core/ollama.js +128 -0
  29. package/dist/src/core/ollama.js.map +1 -0
  30. package/dist/src/core/parser.d.ts +78 -0
  31. package/dist/src/core/parser.js +273 -0
  32. package/dist/src/core/parser.js.map +1 -0
  33. package/dist/src/core/patcher.d.ts +31 -0
  34. package/dist/src/core/patcher.js +128 -0
  35. package/dist/src/core/patcher.js.map +1 -0
  36. package/dist/src/core/project.d.ts +26 -0
  37. package/dist/src/core/project.js +199 -0
  38. package/dist/src/core/project.js.map +1 -0
  39. package/dist/src/core/prompt.d.ts +19 -0
  40. package/dist/src/core/prompt.js +121 -0
  41. package/dist/src/core/prompt.js.map +1 -0
  42. package/dist/src/core/selector.d.ts +46 -0
  43. package/dist/src/core/selector.js +193 -0
  44. package/dist/src/core/selector.js.map +1 -0
  45. package/dist/src/core/state.d.ts +11 -0
  46. package/dist/src/core/state.js +63 -0
  47. package/dist/src/core/state.js.map +1 -0
  48. package/dist/src/core/tools.d.ts +32 -0
  49. package/dist/src/core/tools.js +67 -0
  50. package/dist/src/core/tools.js.map +1 -0
  51. package/dist/src/core/triage.d.ts +37 -0
  52. package/dist/src/core/triage.js +69 -0
  53. package/dist/src/core/triage.js.map +1 -0
  54. package/dist/src/index.d.ts +12 -0
  55. package/dist/src/index.js +120 -0
  56. package/dist/src/index.js.map +1 -0
  57. package/dist/src/lib/compress.d.ts +14 -0
  58. package/dist/src/lib/compress.js +35 -0
  59. package/dist/src/lib/compress.js.map +1 -0
  60. package/dist/src/lib/errors.d.ts +25 -0
  61. package/dist/src/lib/errors.js +32 -0
  62. package/dist/src/lib/errors.js.map +1 -0
  63. package/dist/src/lib/git.d.ts +7 -0
  64. package/dist/src/lib/git.js +45 -0
  65. package/dist/src/lib/git.js.map +1 -0
  66. package/dist/src/lib/runclass.d.ts +24 -0
  67. package/dist/src/lib/runclass.js +66 -0
  68. package/dist/src/lib/runclass.js.map +1 -0
  69. package/dist/src/lib/ui.d.ts +41 -0
  70. package/dist/src/lib/ui.js +92 -0
  71. package/dist/src/lib/ui.js.map +1 -0
  72. package/dist/src/types.d.ts +70 -0
  73. package/dist/src/types.js +7 -0
  74. package/dist/src/types.js.map +1 -0
  75. package/package.json +53 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Minimal client for a local Ollama server (default http://localhost:11434).
3
+ *
4
+ * Design:
5
+ * - Zero runtime dependencies: plain fetch, no SDK.
6
+ * - fetch is injectable so tests never touch a live Ollama and CI passes
7
+ * with no Ollama installed.
8
+ * - Every failure becomes an OllamaError; callers (the selector) catch it
9
+ * and fall back to the heuristic. The LLM path can NEVER break `ask`.
10
+ *
11
+ * This is the v0.2 foundation. It does file-selection reasoning today; the
12
+ * same client will serve later local-LLM tasks (response rescue, triage).
13
+ */
14
+ export class OllamaError extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = 'OllamaError';
18
+ }
19
+ }
20
+ const DEFAULT_REACHABLE_TIMEOUT = 1000; // fast probe; don't hang the user
21
+ const DEFAULT_GENERATE_TIMEOUT = 20000; // generous; off the critical path
22
+ function getFetch(opts) {
23
+ return opts?.fetchImpl ?? globalThis.fetch;
24
+ }
25
+ /**
26
+ * Run a fetch with an abort-based timeout. Throws OllamaError('timed out')
27
+ * if the deadline is hit, or OllamaError(<reason>) on any other failure.
28
+ */
29
+ async function fetchWithTimeout(fetchImpl, url, init, timeoutMs) {
30
+ const controller = new AbortController();
31
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
32
+ try {
33
+ return await fetchImpl(url, { ...init, signal: controller.signal });
34
+ }
35
+ catch (e) {
36
+ if (e.name === 'AbortError') {
37
+ throw new OllamaError(`Ollama request timed out after ${timeoutMs}ms`);
38
+ }
39
+ throw new OllamaError(`Ollama request failed: ${e.message}`);
40
+ }
41
+ finally {
42
+ clearTimeout(timer);
43
+ }
44
+ }
45
+ /**
46
+ * Quick liveness probe. Returns true if Ollama answers /api/tags with 200.
47
+ * Never throws — a down server simply returns false.
48
+ */
49
+ export async function isOllamaReachable(host, opts) {
50
+ const fetchImpl = getFetch(opts);
51
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_REACHABLE_TIMEOUT;
52
+ try {
53
+ const res = await fetchWithTimeout(fetchImpl, `${host}/api/tags`, { method: 'GET' }, timeoutMs);
54
+ return res.ok;
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ /**
61
+ * List the models pulled into Ollama (via GET /api/tags).
62
+ * Throws OllamaError if the server is unreachable or the response is bad.
63
+ */
64
+ export async function listModels(host, opts) {
65
+ const fetchImpl = getFetch(opts);
66
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_REACHABLE_TIMEOUT;
67
+ const res = await fetchWithTimeout(fetchImpl, `${host}/api/tags`, { method: 'GET' }, timeoutMs);
68
+ if (!res.ok) {
69
+ throw new OllamaError(`Ollama /api/tags returned ${res.status}`);
70
+ }
71
+ let body;
72
+ try {
73
+ body = await res.json();
74
+ }
75
+ catch {
76
+ throw new OllamaError('Ollama /api/tags returned invalid JSON');
77
+ }
78
+ const models = body.models ?? [];
79
+ return models
80
+ .map(m => m.name)
81
+ .filter((n) => typeof n === 'string');
82
+ }
83
+ /**
84
+ * Generate a completion via POST /api/chat (non-streaming).
85
+ * Returns the assistant message content.
86
+ *
87
+ * Throws OllamaError on timeout, connection failure, non-2xx status,
88
+ * or unparseable response.
89
+ */
90
+ export async function generate(host, model, prompt, opts) {
91
+ const fetchImpl = getFetch(opts);
92
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_GENERATE_TIMEOUT;
93
+ const res = await fetchWithTimeout(fetchImpl, `${host}/api/chat`, {
94
+ method: 'POST',
95
+ headers: { 'content-type': 'application/json' },
96
+ body: JSON.stringify({
97
+ model,
98
+ stream: false,
99
+ messages: [{ role: 'user', content: prompt }],
100
+ }),
101
+ }, timeoutMs);
102
+ if (!res.ok) {
103
+ // Try to surface Ollama's error message (e.g. model not found)
104
+ let detail = `status ${res.status}`;
105
+ try {
106
+ const body = (await res.json());
107
+ if (body.error)
108
+ detail = body.error;
109
+ }
110
+ catch {
111
+ // ignore parse failure; keep the status-based detail
112
+ }
113
+ throw new OllamaError(`Ollama /api/chat failed: ${detail}`);
114
+ }
115
+ let body;
116
+ try {
117
+ body = await res.json();
118
+ }
119
+ catch {
120
+ throw new OllamaError('Ollama /api/chat returned invalid JSON');
121
+ }
122
+ const content = body.message?.content;
123
+ if (typeof content !== 'string') {
124
+ throw new OllamaError('Ollama /api/chat response missing message content');
125
+ }
126
+ return content.trim();
127
+ }
128
+ //# sourceMappingURL=ollama.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ollama.js","sourceRoot":"","sources":["../../../src/core/ollama.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Parse Claude's structured response per the protocol in docs/PROTOCOL.md.
3
+ *
4
+ * Expected format:
5
+ *
6
+ * ## PLAN
7
+ * one to three sentences
8
+ *
9
+ * ## DIFF: path/to/file.py
10
+ * ```diff
11
+ * --- a/path/to/file.py
12
+ * +++ b/path/to/file.py
13
+ * @@ ... @@
14
+ * - old line
15
+ * + new line
16
+ * ```
17
+ *
18
+ * ## TEST
19
+ * ```bash
20
+ * pytest -k login
21
+ * ```
22
+ *
23
+ * ## NOTES
24
+ * free text
25
+ *
26
+ * Rules:
27
+ * - Every section is optional except `## PLAN`.
28
+ * - Multiple `## DIFF:` blocks are allowed (one per file).
29
+ * - `## TEST` contains a single command.
30
+ * - `## NOTES` is free text shown to the user verbatim.
31
+ * - Code fence lines (```) are stripped from captured content.
32
+ * - If no recognised sections appear, `parseWarning` is set so the caller
33
+ * can ask the user to retry (or run a rescue heuristic).
34
+ */
35
+ export interface DiffHunk {
36
+ filepath: string;
37
+ /** Raw diff lines, including the +/-/space prefix. */
38
+ patchLines: string[];
39
+ }
40
+ export interface ParsedResponse {
41
+ plan: string;
42
+ diffs: DiffHunk[];
43
+ testCommand: string | null;
44
+ notes: string | null;
45
+ /** True if the response didn't follow protocol — needs human review. */
46
+ parseWarning: boolean;
47
+ /**
48
+ * True if auto-repair was applied to fix common copy-paste corruption
49
+ * (missing ## prefixes, merged code fences). Set so the CLI can mention
50
+ * the repair to the user.
51
+ */
52
+ repairsApplied: boolean;
53
+ }
54
+ export declare function parseClaudeResponse(rawText: string): ParsedResponse;
55
+ /**
56
+ * Check a parsed response for things that would prevent safe application.
57
+ * Returns a list of human-readable issues (empty list = looks valid).
58
+ */
59
+ export declare function validateParsedResponse(r: ParsedResponse): string[];
60
+ /**
61
+ * Repair common copy-paste corruption that happens when users drag-select
62
+ * Claude's response from the rendered Claude.ai view instead of using the
63
+ * message's built-in copy icon. Three patterns get fixed:
64
+ *
65
+ * 1. Bare headers — `PLAN` / `TEST` / `NOTES` / `DIFF: <path>` without
66
+ * the leading `## ` get the marker restored.
67
+ * 2. Merged opening fences — `diff--- a/foo` becomes `\`\`\`diff` +
68
+ * `--- a/foo` on separate lines (same for `bashpytest` etc.).
69
+ * 3. Missing closing fences — if we opened a fence to repair (2), insert
70
+ * a closing fence before the next header (or end of input), since the
71
+ * original closing fence was also stripped by the bad copy.
72
+ *
73
+ * Repair is deliberately conservative: it only modifies lines that match
74
+ * recognised patterns at the start of the line (no leading whitespace).
75
+ * Prose that happens to contain "DIFF:" or "diff--" mid-sentence is left
76
+ * alone.
77
+ */
78
+ export declare function repairCorruptedResponse(rawText: string): string;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Parse Claude's structured response per the protocol in docs/PROTOCOL.md.
3
+ *
4
+ * Expected format:
5
+ *
6
+ * ## PLAN
7
+ * one to three sentences
8
+ *
9
+ * ## DIFF: path/to/file.py
10
+ * ```diff
11
+ * --- a/path/to/file.py
12
+ * +++ b/path/to/file.py
13
+ * @@ ... @@
14
+ * - old line
15
+ * + new line
16
+ * ```
17
+ *
18
+ * ## TEST
19
+ * ```bash
20
+ * pytest -k login
21
+ * ```
22
+ *
23
+ * ## NOTES
24
+ * free text
25
+ *
26
+ * Rules:
27
+ * - Every section is optional except `## PLAN`.
28
+ * - Multiple `## DIFF:` blocks are allowed (one per file).
29
+ * - `## TEST` contains a single command.
30
+ * - `## NOTES` is free text shown to the user verbatim.
31
+ * - Code fence lines (```) are stripped from captured content.
32
+ * - If no recognised sections appear, `parseWarning` is set so the caller
33
+ * can ask the user to retry (or run a rescue heuristic).
34
+ */
35
+ export function parseClaudeResponse(rawText) {
36
+ // First pass: try strict parsing on the input as given
37
+ const first = parseStrict(rawText);
38
+ if (!first.parseWarning) {
39
+ return { ...first, repairsApplied: false };
40
+ }
41
+ // Second pass: attempt repair on common copy-paste corruption, re-parse
42
+ const repaired = repairCorruptedResponse(rawText);
43
+ if (repaired === rawText) {
44
+ // Repair didn't change anything; surface the original warning
45
+ return { ...first, repairsApplied: false };
46
+ }
47
+ const second = parseStrict(repaired);
48
+ return { ...second, repairsApplied: true };
49
+ }
50
+ /**
51
+ * Strict parser — exactly the previous (v0.1.0/0.1.1) behaviour.
52
+ * Kept as an internal function so parseClaudeResponse can call it both
53
+ * before and after repair.
54
+ */
55
+ function parseStrict(rawText) {
56
+ const lines = rawText.split(/\r?\n/);
57
+ const result = {
58
+ plan: '',
59
+ diffs: [],
60
+ testCommand: null,
61
+ notes: null,
62
+ parseWarning: false,
63
+ };
64
+ let section = 'NONE';
65
+ let currentFilepath = '';
66
+ let currentPatchLines = [];
67
+ let inCodeFence = false;
68
+ const planLines = [];
69
+ const notesLines = [];
70
+ let testCommandCaptured = null;
71
+ const flushDiff = () => {
72
+ if (currentFilepath && currentPatchLines.length > 0) {
73
+ result.diffs.push({
74
+ filepath: currentFilepath.trim(),
75
+ patchLines: currentPatchLines,
76
+ });
77
+ }
78
+ currentFilepath = '';
79
+ currentPatchLines = [];
80
+ };
81
+ for (const line of lines) {
82
+ // Section headers only count outside code fences
83
+ if (!inCodeFence) {
84
+ if (/^##\s+PLAN\b/.test(line)) {
85
+ flushDiff();
86
+ section = 'PLAN';
87
+ continue;
88
+ }
89
+ const diffMatch = line.match(/^##\s+DIFF:\s*(.+)/);
90
+ if (diffMatch) {
91
+ flushDiff();
92
+ section = 'DIFF';
93
+ currentFilepath = diffMatch[1].trim();
94
+ continue;
95
+ }
96
+ if (/^##\s+TEST\b/.test(line)) {
97
+ flushDiff();
98
+ section = 'TEST';
99
+ continue;
100
+ }
101
+ if (/^##\s+NOTES\b/.test(line)) {
102
+ flushDiff();
103
+ section = 'NOTES';
104
+ continue;
105
+ }
106
+ }
107
+ // Code fence toggle
108
+ if (/^```/.test(line)) {
109
+ inCodeFence = !inCodeFence;
110
+ continue;
111
+ }
112
+ switch (section) {
113
+ case 'PLAN':
114
+ planLines.push(line);
115
+ break;
116
+ case 'DIFF':
117
+ currentPatchLines.push(line);
118
+ break;
119
+ case 'TEST':
120
+ if (line.trim() && testCommandCaptured === null) {
121
+ testCommandCaptured = line.trim();
122
+ }
123
+ break;
124
+ case 'NOTES':
125
+ notesLines.push(line);
126
+ break;
127
+ case 'NONE':
128
+ // Lines outside any section are ignored
129
+ break;
130
+ }
131
+ }
132
+ flushDiff();
133
+ result.plan = planLines.join('\n').trim();
134
+ result.testCommand = testCommandCaptured;
135
+ result.notes = notesLines.length > 0 ? notesLines.join('\n').trim() : null;
136
+ if (!result.plan && result.diffs.length === 0) {
137
+ result.parseWarning = true;
138
+ }
139
+ return result;
140
+ }
141
+ /**
142
+ * Check a parsed response for things that would prevent safe application.
143
+ * Returns a list of human-readable issues (empty list = looks valid).
144
+ */
145
+ export function validateParsedResponse(r) {
146
+ const issues = [];
147
+ if (!r.plan)
148
+ issues.push('Missing ## PLAN section');
149
+ for (const d of r.diffs) {
150
+ if (!d.filepath)
151
+ issues.push('A ## DIFF: block is missing a filepath');
152
+ if (d.patchLines.length === 0)
153
+ issues.push(`Empty diff for ${d.filepath}`);
154
+ }
155
+ return issues;
156
+ }
157
+ // ---------- v0.1.2 auto-repair ----------
158
+ /** Section markers we know about, used to detect bare headers. */
159
+ const BARE_HEADERS = ['PLAN', 'TEST', 'NOTES'];
160
+ /**
161
+ * Language identifiers that commonly appear as the first token of a merged
162
+ * fence line (e.g. `diff--- a/foo` came from ` ```diff ` + `--- a/foo`).
163
+ * Order matters: longer/more-specific identifiers come first so we don't
164
+ * mistake `pythondef` for a `py`-prefixed fence.
165
+ */
166
+ const FENCE_LANGS = [
167
+ 'typescript', 'javascript', 'python', 'bash', 'diff', 'json', 'yaml',
168
+ 'yml', 'jsx', 'tsx', 'sh', 'js', 'ts', 'py', 'go', 'rs', 'rb',
169
+ ];
170
+ /**
171
+ * Repair common copy-paste corruption that happens when users drag-select
172
+ * Claude's response from the rendered Claude.ai view instead of using the
173
+ * message's built-in copy icon. Three patterns get fixed:
174
+ *
175
+ * 1. Bare headers — `PLAN` / `TEST` / `NOTES` / `DIFF: <path>` without
176
+ * the leading `## ` get the marker restored.
177
+ * 2. Merged opening fences — `diff--- a/foo` becomes `\`\`\`diff` +
178
+ * `--- a/foo` on separate lines (same for `bashpytest` etc.).
179
+ * 3. Missing closing fences — if we opened a fence to repair (2), insert
180
+ * a closing fence before the next header (or end of input), since the
181
+ * original closing fence was also stripped by the bad copy.
182
+ *
183
+ * Repair is deliberately conservative: it only modifies lines that match
184
+ * recognised patterns at the start of the line (no leading whitespace).
185
+ * Prose that happens to contain "DIFF:" or "diff--" mid-sentence is left
186
+ * alone.
187
+ */
188
+ export function repairCorruptedResponse(rawText) {
189
+ const lines = rawText.split(/\r?\n/);
190
+ const pass1 = [];
191
+ for (const line of lines) {
192
+ if (isBareHeader(line)) {
193
+ pass1.push({ content: '## ' + line, isHeader: true, openedFence: false });
194
+ continue;
195
+ }
196
+ const split = splitMergedFence(line);
197
+ if (split) {
198
+ pass1.push({ content: '```' + split.lang, isHeader: false, openedFence: true });
199
+ pass1.push({ content: split.rest, isHeader: false, openedFence: false });
200
+ continue;
201
+ }
202
+ pass1.push({ content: line, isHeader: false, openedFence: false });
203
+ }
204
+ // Second pass: walk forward; when we encounter an opening fence that we
205
+ // inserted, scan ahead for the next header (or end-of-input) and insert
206
+ // a closing fence right before it.
207
+ const out = [];
208
+ let needsClose = false;
209
+ for (let i = 0; i < pass1.length; i++) {
210
+ const t = pass1[i];
211
+ if (needsClose && t.isHeader) {
212
+ // Close the fence before this header. Strip a trailing blank line so
213
+ // we don't leave an awkward gap inside the code block.
214
+ if (out.length > 0 && out[out.length - 1] === '')
215
+ out.pop();
216
+ out.push('```');
217
+ out.push(''); // blank line between fence and header
218
+ needsClose = false;
219
+ }
220
+ out.push(t.content);
221
+ if (t.openedFence)
222
+ needsClose = true;
223
+ }
224
+ if (needsClose) {
225
+ if (out.length > 0 && out[out.length - 1] === '')
226
+ out.pop();
227
+ out.push('```');
228
+ }
229
+ return out.join('\n');
230
+ }
231
+ /**
232
+ * Is this line a bare section header? Headers must occupy the whole line —
233
+ * no leading/trailing whitespace, nothing else on the line. This is the
234
+ * guard that keeps repair from triggering on prose like "the DIFF: header".
235
+ */
236
+ function isBareHeader(line) {
237
+ if (BARE_HEADERS.includes(line))
238
+ return true;
239
+ // DIFF: <path> — path must look like a filepath (has a dot or slash)
240
+ const m = line.match(/^DIFF:\s+(\S+)$/);
241
+ if (m && (m[1].includes('/') || m[1].includes('.')))
242
+ return true;
243
+ return false;
244
+ }
245
+ /**
246
+ * If this line is a merged opening fence ("diff--- ...", "bashpytest"),
247
+ * return the language + the rest of the line as two pieces to be emitted
248
+ * on separate lines. Otherwise return null.
249
+ *
250
+ * Only triggers when the language is followed by content that looks like
251
+ * code, not arbitrary prose, to avoid false positives.
252
+ */
253
+ function splitMergedFence(line) {
254
+ for (const lang of FENCE_LANGS) {
255
+ if (line.startsWith(lang) && line.length > lang.length) {
256
+ const rest = line.slice(lang.length);
257
+ // The character right after the language must look like the start of
258
+ // code, not the next word of prose. Whitelist a few common starts:
259
+ // - `-` (diff marker)
260
+ // - `+` (diff marker)
261
+ // - `@` (diff hunk header)
262
+ // - a letter/digit (most common command/code starts: `pytest`, `def`, etc.)
263
+ // Reject if the next char is a space (already separated) or punctuation
264
+ // unlikely at the start of code.
265
+ const next = rest[0];
266
+ if (/[-+@A-Za-z0-9_/]/.test(next)) {
267
+ return { lang, rest };
268
+ }
269
+ }
270
+ }
271
+ return null;
272
+ }
273
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../../../src/core/parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Line-by-line diff application.
3
+ *
4
+ * Design principles:
5
+ * - Never silently corrupt: throw HelpcodeError on any ambiguity.
6
+ * - Atomic writes: temp file + rename, so a kill mid-write doesn't
7
+ * leave a half-written source file.
8
+ * - New files are created with their parent directory if needed.
9
+ * - Caller is responsible for snapshotting before patching if rollback is wanted.
10
+ *
11
+ * Patch format:
12
+ * Each line starts with ' ' (context), '-' (removed), or '+' (added).
13
+ * Order matters — context and removal lines together describe what the
14
+ * file looks like NOW; context and add lines describe what it should
15
+ * look like AFTER.
16
+ */
17
+ export interface PatchResult {
18
+ filepath: string;
19
+ hunksApplied: number;
20
+ created: boolean;
21
+ ok: boolean;
22
+ }
23
+ /**
24
+ * Apply an array of unified-diff lines (+, -, space prefixes) to a file.
25
+ *
26
+ * Throws HelpcodeError on:
27
+ * - File missing but diff includes context/removal (couldn't anchor)
28
+ * - Anchor lines don't match anywhere in the target
29
+ * - Write fails
30
+ */
31
+ export declare function applyLinePatch(filepath: string, patchLines: string[]): PatchResult;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Line-by-line diff application.
3
+ *
4
+ * Design principles:
5
+ * - Never silently corrupt: throw HelpcodeError on any ambiguity.
6
+ * - Atomic writes: temp file + rename, so a kill mid-write doesn't
7
+ * leave a half-written source file.
8
+ * - New files are created with their parent directory if needed.
9
+ * - Caller is responsible for snapshotting before patching if rollback is wanted.
10
+ *
11
+ * Patch format:
12
+ * Each line starts with ' ' (context), '-' (removed), or '+' (added).
13
+ * Order matters — context and removal lines together describe what the
14
+ * file looks like NOW; context and add lines describe what it should
15
+ * look like AFTER.
16
+ */
17
+ import * as fs from 'fs';
18
+ import * as path from 'path';
19
+ import { HelpcodeError, ErrorCode } from '../lib/errors.js';
20
+ /**
21
+ * Apply an array of unified-diff lines (+, -, space prefixes) to a file.
22
+ *
23
+ * Throws HelpcodeError on:
24
+ * - File missing but diff includes context/removal (couldn't anchor)
25
+ * - Anchor lines don't match anywhere in the target
26
+ * - Write fails
27
+ */
28
+ export function applyLinePatch(filepath, patchLines) {
29
+ // Strip diff header lines like `--- a/foo` / `+++ b/foo` / `@@ ... @@`
30
+ // that Claude often includes inside the fenced ```diff block.
31
+ const cleaned = stripDiffHeaders(patchLines);
32
+ // New-file case: only `+` and blank/`---,+++` lines, target doesn't exist
33
+ const looksLikeNewFile = cleaned.every(l => l.startsWith('+') || l === '') &&
34
+ !fs.existsSync(filepath);
35
+ if (looksLikeNewFile) {
36
+ const content = cleaned
37
+ .filter(l => l.startsWith('+'))
38
+ .map(l => l.slice(1))
39
+ .join('\n');
40
+ const dir = path.dirname(filepath);
41
+ if (dir && dir !== '.')
42
+ fs.mkdirSync(dir, { recursive: true });
43
+ atomicWrite(filepath, content);
44
+ return { filepath, hunksApplied: 1, created: true, ok: true };
45
+ }
46
+ if (!fs.existsSync(filepath)) {
47
+ throw new HelpcodeError(ErrorCode.IO_ERROR, `File not found: ${filepath}`, 'Check the path in the diff header. Run `helpcode status` to see current task.');
48
+ }
49
+ const fileContent = fs.readFileSync(filepath, 'utf-8');
50
+ const targetLines = fileContent.split(/\r?\n/);
51
+ // Build the "before" sequence: context + removal lines, in order.
52
+ // Build the "after" sequence: context + add lines, in order.
53
+ const before = [];
54
+ const after = [];
55
+ for (const line of cleaned) {
56
+ if (line.startsWith(' ')) {
57
+ before.push(line.slice(1));
58
+ after.push(line.slice(1));
59
+ }
60
+ else if (line.startsWith('-')) {
61
+ before.push(line.slice(1));
62
+ }
63
+ else if (line.startsWith('+')) {
64
+ after.push(line.slice(1));
65
+ }
66
+ // Blank lines (rare in proper diffs) ignored.
67
+ }
68
+ if (before.length === 0) {
69
+ // Pure addition with no context — ambiguous, refuse
70
+ throw new HelpcodeError(ErrorCode.VALIDATION_ERROR, `Patch for ${filepath} has no context or removal lines to anchor against.`, 'Ask Claude for a fresh diff that includes a few unchanged context lines.');
71
+ }
72
+ const anchor = findAnchor(targetLines, before);
73
+ if (anchor === -1) {
74
+ throw new HelpcodeError(ErrorCode.VALIDATION_ERROR, `Patch does not apply cleanly to ${filepath}: anchor lines not found.`, 'The file may have changed since Claude wrote the diff. Re-run `helpcode ask` for a fresh one.');
75
+ }
76
+ const result = [
77
+ ...targetLines.slice(0, anchor),
78
+ ...after,
79
+ ...targetLines.slice(anchor + before.length),
80
+ ];
81
+ atomicWrite(filepath, result.join('\n'));
82
+ return { filepath, hunksApplied: 1, created: false, ok: true };
83
+ }
84
+ /** Drop diff header lines like `--- a/foo`, `+++ b/foo`, `@@ ... @@`. */
85
+ function stripDiffHeaders(patchLines) {
86
+ return patchLines.filter(line => {
87
+ if (line.startsWith('--- ') || line.startsWith('+++ '))
88
+ return false;
89
+ if (line.startsWith('@@'))
90
+ return false;
91
+ return true;
92
+ });
93
+ }
94
+ function findAnchor(target, pattern) {
95
+ if (pattern.length === 0)
96
+ return -1;
97
+ for (let i = 0; i <= target.length - pattern.length; i++) {
98
+ let match = true;
99
+ for (let k = 0; k < pattern.length; k++) {
100
+ if (target[i + k] !== pattern[k]) {
101
+ match = false;
102
+ break;
103
+ }
104
+ }
105
+ if (match)
106
+ return i;
107
+ }
108
+ return -1;
109
+ }
110
+ /**
111
+ * Write content via a temp file + rename. Prevents partial-write corruption
112
+ * if the process is killed mid-write.
113
+ */
114
+ function atomicWrite(filepath, content) {
115
+ const tmp = `${filepath}.${Math.random().toString(36).slice(2)}.tmp`;
116
+ try {
117
+ fs.writeFileSync(tmp, content, 'utf-8');
118
+ fs.renameSync(tmp, filepath);
119
+ }
120
+ catch (e) {
121
+ try {
122
+ fs.unlinkSync(tmp);
123
+ }
124
+ catch { /* ignore */ }
125
+ throw new HelpcodeError(ErrorCode.IO_ERROR, `Atomic write failed for ${filepath}: ${e.message}`, 'Check disk space and file permissions.');
126
+ }
127
+ }
128
+ //# sourceMappingURL=patcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patcher.js","sourceRoot":"","sources":["../../../src/core/patcher.ts"],"names":[],"mappings":"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"}