@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.
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/bin/helpcode.d.ts +5 -0
- package/dist/bin/helpcode.js +10 -0
- package/dist/bin/helpcode.js.map +1 -0
- package/dist/src/commands/apply.d.ts +15 -0
- package/dist/src/commands/apply.js +195 -0
- package/dist/src/commands/apply.js.map +1 -0
- package/dist/src/commands/ask.d.ts +12 -0
- package/dist/src/commands/ask.js +93 -0
- package/dist/src/commands/ask.js.map +1 -0
- package/dist/src/commands/init.d.ts +13 -0
- package/dist/src/commands/init.js +91 -0
- package/dist/src/commands/init.js.map +1 -0
- package/dist/src/commands/reset.d.ts +8 -0
- package/dist/src/commands/reset.js +19 -0
- package/dist/src/commands/reset.js.map +1 -0
- package/dist/src/commands/run.d.ts +15 -0
- package/dist/src/commands/run.js +109 -0
- package/dist/src/commands/run.js.map +1 -0
- package/dist/src/commands/status.d.ts +4 -0
- package/dist/src/commands/status.js +53 -0
- package/dist/src/commands/status.js.map +1 -0
- package/dist/src/core/llmSelector.d.ts +65 -0
- package/dist/src/core/llmSelector.js +134 -0
- package/dist/src/core/llmSelector.js.map +1 -0
- package/dist/src/core/ollama.d.ts +43 -0
- package/dist/src/core/ollama.js +128 -0
- package/dist/src/core/ollama.js.map +1 -0
- package/dist/src/core/parser.d.ts +78 -0
- package/dist/src/core/parser.js +273 -0
- package/dist/src/core/parser.js.map +1 -0
- package/dist/src/core/patcher.d.ts +31 -0
- package/dist/src/core/patcher.js +128 -0
- package/dist/src/core/patcher.js.map +1 -0
- package/dist/src/core/project.d.ts +26 -0
- package/dist/src/core/project.js +199 -0
- package/dist/src/core/project.js.map +1 -0
- package/dist/src/core/prompt.d.ts +19 -0
- package/dist/src/core/prompt.js +121 -0
- package/dist/src/core/prompt.js.map +1 -0
- package/dist/src/core/selector.d.ts +46 -0
- package/dist/src/core/selector.js +193 -0
- package/dist/src/core/selector.js.map +1 -0
- package/dist/src/core/state.d.ts +11 -0
- package/dist/src/core/state.js +63 -0
- package/dist/src/core/state.js.map +1 -0
- package/dist/src/core/tools.d.ts +32 -0
- package/dist/src/core/tools.js +67 -0
- package/dist/src/core/tools.js.map +1 -0
- package/dist/src/core/triage.d.ts +37 -0
- package/dist/src/core/triage.js +69 -0
- package/dist/src/core/triage.js.map +1 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.js +120 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lib/compress.d.ts +14 -0
- package/dist/src/lib/compress.js +35 -0
- package/dist/src/lib/compress.js.map +1 -0
- package/dist/src/lib/errors.d.ts +25 -0
- package/dist/src/lib/errors.js +32 -0
- package/dist/src/lib/errors.js.map +1 -0
- package/dist/src/lib/git.d.ts +7 -0
- package/dist/src/lib/git.js +45 -0
- package/dist/src/lib/git.js.map +1 -0
- package/dist/src/lib/runclass.d.ts +24 -0
- package/dist/src/lib/runclass.js +66 -0
- package/dist/src/lib/runclass.js.map +1 -0
- package/dist/src/lib/ui.d.ts +41 -0
- package/dist/src/lib/ui.js +92 -0
- package/dist/src/lib/ui.js.map +1 -0
- package/dist/src/types.d.ts +70 -0
- package/dist/src/types.js +7 -0
- package/dist/src/types.js.map +1 -0
- 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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"}
|