@glbgpt/cli 0.1.4 → 0.2.1
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/README.md +82 -40
- package/dist/src/coding/backgroundTasks.d.ts +34 -0
- package/dist/src/coding/backgroundTasks.js +154 -0
- package/dist/src/coding/backgroundTasks.js.map +1 -0
- package/dist/src/coding/backgroundTools.d.ts +9 -0
- package/dist/src/coding/backgroundTools.js +63 -0
- package/dist/src/coding/backgroundTools.js.map +1 -0
- package/dist/src/coding/bashTool.d.ts +27 -0
- package/dist/src/coding/bashTool.js +144 -0
- package/dist/src/coding/bashTool.js.map +1 -0
- package/dist/src/coding/dangerousCommands.d.ts +29 -0
- package/dist/src/coding/dangerousCommands.js +156 -0
- package/dist/src/coding/dangerousCommands.js.map +1 -0
- package/dist/src/coding/editDiff.d.ts +45 -0
- package/dist/src/coding/editDiff.js +156 -0
- package/dist/src/coding/editDiff.js.map +1 -0
- package/dist/src/coding/editTool.d.ts +14 -0
- package/dist/src/coding/editTool.js +154 -0
- package/dist/src/coding/editTool.js.map +1 -0
- package/dist/src/coding/index.d.ts +25 -0
- package/dist/src/coding/index.js +30 -0
- package/dist/src/coding/index.js.map +1 -0
- package/dist/src/coding/outputAccumulator.d.ts +34 -0
- package/dist/src/coding/outputAccumulator.js +111 -0
- package/dist/src/coding/outputAccumulator.js.map +1 -0
- package/dist/src/coding/permissionGate.d.ts +65 -0
- package/dist/src/coding/permissionGate.js +138 -0
- package/dist/src/coding/permissionGate.js.map +1 -0
- package/dist/src/coding/permissionStore.d.ts +36 -0
- package/dist/src/coding/permissionStore.js +56 -0
- package/dist/src/coding/permissionStore.js.map +1 -0
- package/dist/src/coding/permissions.d.ts +34 -0
- package/dist/src/coding/permissions.js +18 -0
- package/dist/src/coding/permissions.js.map +1 -0
- package/dist/src/coding/projectContext.d.ts +21 -0
- package/dist/src/coding/projectContext.js +118 -0
- package/dist/src/coding/projectContext.js.map +1 -0
- package/dist/src/coding/readState.d.ts +22 -0
- package/dist/src/coding/readState.js +30 -0
- package/dist/src/coding/readState.js.map +1 -0
- package/dist/src/coding/truncate.d.ts +39 -0
- package/dist/src/coding/truncate.js +83 -0
- package/dist/src/coding/truncate.js.map +1 -0
- package/dist/src/coding/writeTool.d.ts +14 -0
- package/dist/src/coding/writeTool.js +82 -0
- package/dist/src/coding/writeTool.js.map +1 -0
- package/dist/src/commands/execCommand.d.ts +13 -0
- package/dist/src/commands/execCommand.js +73 -6
- package/dist/src/commands/execCommand.js.map +1 -1
- package/dist/src/commands/updateCommand.d.ts +7 -0
- package/dist/src/commands/updateCommand.js +11 -0
- package/dist/src/commands/updateCommand.js.map +1 -0
- package/dist/src/core/catalog.d.ts +5 -0
- package/dist/src/core/catalog.js +31 -1
- package/dist/src/core/catalog.js.map +1 -1
- package/dist/src/core/compaction.d.ts +52 -0
- package/dist/src/core/compaction.js +151 -0
- package/dist/src/core/compaction.js.map +1 -0
- package/dist/src/core/contextPrune.js +8 -1
- package/dist/src/core/contextPrune.js.map +1 -1
- package/dist/src/core/runChat.d.ts +1 -1
- package/dist/src/core/runChat.js +1 -1
- package/dist/src/core/runChat.js.map +1 -1
- package/dist/src/core/session.d.ts +10 -1
- package/dist/src/core/session.js +11 -2
- package/dist/src/core/session.js.map +1 -1
- package/dist/src/core/turnLoop.d.ts +18 -2
- package/dist/src/core/turnLoop.js +79 -12
- package/dist/src/core/turnLoop.js.map +1 -1
- package/dist/src/core/updateCheck.d.ts +35 -0
- package/dist/src/core/updateCheck.js +71 -2
- package/dist/src/core/updateCheck.js.map +1 -1
- package/dist/src/core/updateRunner.d.ts +7 -0
- package/dist/src/core/updateRunner.js +26 -0
- package/dist/src/core/updateRunner.js.map +1 -0
- package/dist/src/exec/output.d.ts +3 -2
- package/dist/src/exec/output.js +17 -4
- package/dist/src/exec/output.js.map +1 -1
- package/dist/src/fs/index.d.ts +4 -1
- package/dist/src/fs/index.js +7 -2
- package/dist/src/fs/index.js.map +1 -1
- package/dist/src/fs/limits.d.ts +1 -1
- package/dist/src/fs/limits.js +3 -1
- package/dist/src/fs/limits.js.map +1 -1
- package/dist/src/fs/readFileTool.d.ts +2 -1
- package/dist/src/fs/readFileTool.js +4 -1
- package/dist/src/fs/readFileTool.js.map +1 -1
- package/dist/src/fs/workspaceGuard.d.ts +8 -0
- package/dist/src/fs/workspaceGuard.js +48 -0
- package/dist/src/fs/workspaceGuard.js.map +1 -1
- package/dist/src/index.js +44 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/server.js +17 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/tui/App.d.ts +10 -1
- package/dist/src/tui/App.js +114 -15
- package/dist/src/tui/App.js.map +1 -1
- package/dist/src/tui/UpdatePrompt.d.ts +17 -0
- package/dist/src/tui/UpdatePrompt.js +42 -0
- package/dist/src/tui/UpdatePrompt.js.map +1 -0
- package/dist/src/tui/approvalBridge.d.ts +21 -0
- package/dist/src/tui/approvalBridge.js +45 -0
- package/dist/src/tui/approvalBridge.js.map +1 -0
- package/dist/src/tui/chatController.d.ts +21 -0
- package/dist/src/tui/chatController.js +155 -16
- package/dist/src/tui/chatController.js.map +1 -1
- package/dist/src/tui/commands.js +16 -2
- package/dist/src/tui/commands.js.map +1 -1
- package/dist/src/tui/models.d.ts +4 -0
- package/dist/src/tui/models.js +8 -1
- package/dist/src/tui/models.js.map +1 -1
- package/dist/src/tui/run.d.ts +7 -0
- package/dist/src/tui/run.js +107 -22
- package/dist/src/tui/run.js.map +1 -1
- package/dist/src/web/webFetchTool.js +41 -5
- package/dist/src/web/webFetchTool.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dangerous-command heuristics for the bash approval gate (codex lineage,
|
|
3
|
+
* 2026-08-21 dossier). Two independent judgments:
|
|
4
|
+
*
|
|
5
|
+
* - {@link isDangerousCommand}: commands that must ALWAYS be confirmed one at a
|
|
6
|
+
* time and can never enter the session allowlist (forced/recursive rm, sudo,
|
|
7
|
+
* raw device writes...). Wrappers (sudo/env/nice/...) are recursively unwrapped
|
|
8
|
+
* with a depth cap that fails CLOSED (too deeply wrapped = dangerous).
|
|
9
|
+
*
|
|
10
|
+
* - {@link isBannedAllowlistPrefix}: prefixes that must never become standing
|
|
11
|
+
* allowlist rules because approving them approves arbitrary code execution
|
|
12
|
+
* (bare shells, interpreter evals, env, rm, bare git/npm...). The command can
|
|
13
|
+
* still be approved ONCE — it just never gets a "don't ask again" rule.
|
|
14
|
+
*/
|
|
15
|
+
/** Naive whitespace tokenizer that keeps quoted spans intact (conservative — good enough for judging). */
|
|
16
|
+
export declare function tokenize(command: string): string[];
|
|
17
|
+
/** The effective base tokens after unwrapping (depth-capped). Null = too wrapped, treat as dangerous. */
|
|
18
|
+
export declare function unwrapCommand(command: string): string[] | null;
|
|
19
|
+
/** True when the command contains shell control operators / redirections (top-level or not — conservative). */
|
|
20
|
+
export declare function isCompoundCommand(command: string): boolean;
|
|
21
|
+
export declare function isDangerousCommand(command: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The standing-allowlist prefix for a command: its first two tokens (or the single
|
|
24
|
+
* token for a one-token command). Returns null when no safe prefix exists — the
|
|
25
|
+
* command may still be approved once, but never remembered.
|
|
26
|
+
*/
|
|
27
|
+
export declare function allowlistPrefixFor(command: string): string | null;
|
|
28
|
+
/** Claude Code prefix-match semantics: word boundary, and NEVER against a compound command. */
|
|
29
|
+
export declare function matchesAllowlistPrefix(command: string, prefix: string): boolean;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dangerous-command heuristics for the bash approval gate (codex lineage,
|
|
3
|
+
* 2026-08-21 dossier). Two independent judgments:
|
|
4
|
+
*
|
|
5
|
+
* - {@link isDangerousCommand}: commands that must ALWAYS be confirmed one at a
|
|
6
|
+
* time and can never enter the session allowlist (forced/recursive rm, sudo,
|
|
7
|
+
* raw device writes...). Wrappers (sudo/env/nice/...) are recursively unwrapped
|
|
8
|
+
* with a depth cap that fails CLOSED (too deeply wrapped = dangerous).
|
|
9
|
+
*
|
|
10
|
+
* - {@link isBannedAllowlistPrefix}: prefixes that must never become standing
|
|
11
|
+
* allowlist rules because approving them approves arbitrary code execution
|
|
12
|
+
* (bare shells, interpreter evals, env, rm, bare git/npm...). The command can
|
|
13
|
+
* still be approved ONCE — it just never gets a "don't ask again" rule.
|
|
14
|
+
*/
|
|
15
|
+
const WRAPPERS = new Set(['sudo', 'doas', 'env', 'nice', 'nohup', 'time', 'stdbuf', 'timeout', 'command']);
|
|
16
|
+
/** First tokens that make a standing allowlist rule equivalent to "allow everything". */
|
|
17
|
+
const BANNED_PREFIX_FIRST_TOKENS = new Set([
|
|
18
|
+
'sh',
|
|
19
|
+
'bash',
|
|
20
|
+
'zsh',
|
|
21
|
+
'dash',
|
|
22
|
+
'ksh',
|
|
23
|
+
'fish',
|
|
24
|
+
'pwsh',
|
|
25
|
+
'powershell',
|
|
26
|
+
'node',
|
|
27
|
+
'python',
|
|
28
|
+
'python3',
|
|
29
|
+
'perl',
|
|
30
|
+
'ruby',
|
|
31
|
+
'php',
|
|
32
|
+
'deno',
|
|
33
|
+
'bun',
|
|
34
|
+
'npx',
|
|
35
|
+
'uvx',
|
|
36
|
+
'eval',
|
|
37
|
+
'exec',
|
|
38
|
+
'source',
|
|
39
|
+
'xargs',
|
|
40
|
+
'sudo',
|
|
41
|
+
'doas',
|
|
42
|
+
'env',
|
|
43
|
+
'rm',
|
|
44
|
+
'ssh',
|
|
45
|
+
]);
|
|
46
|
+
/** Broad tool launchers: a ONE-word prefix is too permissive, two words (git status) is fine. */
|
|
47
|
+
const TOO_BROAD_ALONE = new Set(['git', 'npm', 'yarn', 'pnpm', 'docker', 'kubectl', 'make', 'mvn', 'gradle']);
|
|
48
|
+
/** Naive whitespace tokenizer that keeps quoted spans intact (conservative — good enough for judging). */
|
|
49
|
+
export function tokenize(command) {
|
|
50
|
+
const tokens = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
51
|
+
return tokens.map((t) => t.trim()).filter((t) => t.length > 0);
|
|
52
|
+
}
|
|
53
|
+
/** Drop leading VAR=value assignments (so `FOO=1 rm -rf /` is judged as rm). */
|
|
54
|
+
function stripAssignments(tokens) {
|
|
55
|
+
let i = 0;
|
|
56
|
+
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i]))
|
|
57
|
+
i += 1;
|
|
58
|
+
return tokens.slice(i);
|
|
59
|
+
}
|
|
60
|
+
/** Unwrap one wrapper layer (sudo/env/... plus their obvious flags/operands). */
|
|
61
|
+
function unwrapOnce(tokens) {
|
|
62
|
+
const t = stripAssignments(tokens);
|
|
63
|
+
if (t.length === 0 || !WRAPPERS.has(t[0]))
|
|
64
|
+
return t === tokens ? null : t;
|
|
65
|
+
let rest = t.slice(1);
|
|
66
|
+
// Skip wrapper flags; for `timeout` also its duration operand; for env, assignments.
|
|
67
|
+
while (rest.length > 0 && (rest[0].startsWith('-') || /^[A-Za-z_][A-Za-z0-9_]*=/.test(rest[0]))) {
|
|
68
|
+
rest = rest.slice(1);
|
|
69
|
+
}
|
|
70
|
+
if (t[0] === 'timeout' && rest.length > 0 && /^\d/.test(rest[0]))
|
|
71
|
+
rest = rest.slice(1);
|
|
72
|
+
return rest;
|
|
73
|
+
}
|
|
74
|
+
/** The effective base tokens after unwrapping (depth-capped). Null = too wrapped, treat as dangerous. */
|
|
75
|
+
export function unwrapCommand(command) {
|
|
76
|
+
let tokens = tokenize(command);
|
|
77
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
78
|
+
const next = unwrapOnce(tokens);
|
|
79
|
+
if (next === null)
|
|
80
|
+
return tokens;
|
|
81
|
+
if (next.length === 0)
|
|
82
|
+
return next;
|
|
83
|
+
if (next === tokens || next[0] === tokens[0])
|
|
84
|
+
return next;
|
|
85
|
+
tokens = next;
|
|
86
|
+
}
|
|
87
|
+
return null; // fail closed
|
|
88
|
+
}
|
|
89
|
+
/** True when the command contains shell control operators / redirections (top-level or not — conservative). */
|
|
90
|
+
export function isCompoundCommand(command) {
|
|
91
|
+
return /(\|\||&&|;|\||&|>|<|\n|`|\$\()/.test(command);
|
|
92
|
+
}
|
|
93
|
+
function rmIsForcedOrRecursive(tokens) {
|
|
94
|
+
return tokens.some((t) => t === '--force' ||
|
|
95
|
+
t === '--recursive' ||
|
|
96
|
+
(/^-[a-zA-Z]+$/.test(t) && (t.includes('r') || t.includes('R') || t.includes('f'))));
|
|
97
|
+
}
|
|
98
|
+
/** Segment a compound command on control operators so each piece is judged separately. */
|
|
99
|
+
function segments(command) {
|
|
100
|
+
return command
|
|
101
|
+
.split(/\|\||&&|;|\||\n/)
|
|
102
|
+
.map((s) => s.trim())
|
|
103
|
+
.filter((s) => s.length > 0);
|
|
104
|
+
}
|
|
105
|
+
export function isDangerousCommand(command) {
|
|
106
|
+
for (const seg of segments(command)) {
|
|
107
|
+
const tokens = unwrapCommand(seg);
|
|
108
|
+
if (tokens === null)
|
|
109
|
+
return true; // unwrap depth exhausted — fail closed
|
|
110
|
+
if (tokens.length === 0)
|
|
111
|
+
continue;
|
|
112
|
+
const raw = tokenize(seg);
|
|
113
|
+
const base = tokens[0];
|
|
114
|
+
if (raw[0] === 'sudo' || raw[0] === 'doas')
|
|
115
|
+
return true;
|
|
116
|
+
if (base === 'rm' && rmIsForcedOrRecursive(tokens))
|
|
117
|
+
return true;
|
|
118
|
+
if (base === 'mkfs' || base.startsWith('mkfs.'))
|
|
119
|
+
return true;
|
|
120
|
+
if (base === 'shutdown' || base === 'reboot' || base === 'halt')
|
|
121
|
+
return true;
|
|
122
|
+
if (base === 'dd' && tokens.some((t) => t.startsWith('of=/dev/')))
|
|
123
|
+
return true;
|
|
124
|
+
if ((base === 'chmod' || base === 'chown') && tokens.some((t) => t === '777' || t.endsWith(':777'))) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The standing-allowlist prefix for a command: its first two tokens (or the single
|
|
132
|
+
* token for a one-token command). Returns null when no safe prefix exists — the
|
|
133
|
+
* command may still be approved once, but never remembered.
|
|
134
|
+
*/
|
|
135
|
+
export function allowlistPrefixFor(command) {
|
|
136
|
+
if (isCompoundCommand(command) || isDangerousCommand(command))
|
|
137
|
+
return null;
|
|
138
|
+
const tokens = tokenize(command);
|
|
139
|
+
if (tokens.length === 0)
|
|
140
|
+
return null;
|
|
141
|
+
if (BANNED_PREFIX_FIRST_TOKENS.has(tokens[0]))
|
|
142
|
+
return null;
|
|
143
|
+
if (tokens.length === 1) {
|
|
144
|
+
return TOO_BROAD_ALONE.has(tokens[0]) ? null : tokens[0];
|
|
145
|
+
}
|
|
146
|
+
if (tokens[1].startsWith('-'))
|
|
147
|
+
return tokens[0]; // flags are not a subcommand
|
|
148
|
+
return `${tokens[0]} ${tokens[1]}`;
|
|
149
|
+
}
|
|
150
|
+
/** Claude Code prefix-match semantics: word boundary, and NEVER against a compound command. */
|
|
151
|
+
export function matchesAllowlistPrefix(command, prefix) {
|
|
152
|
+
if (isCompoundCommand(command))
|
|
153
|
+
return false;
|
|
154
|
+
return command === prefix || command.startsWith(prefix + ' ');
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=dangerousCommands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dangerousCommands.js","sourceRoot":"","sources":["../../../src/coding/dangerousCommands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matching + application logic for edit_file (pi contract, 2026-08-21 dossier):
|
|
3
|
+
* every edit matches against the ORIGINAL file, must be found exactly once, edits
|
|
4
|
+
* must not overlap, and the combined result must differ from the original. Exact
|
|
5
|
+
* matching first; a conservative line-based fuzzy fallback absorbs the classic
|
|
6
|
+
* model slips (smart quotes, unicode dashes/spaces, trailing whitespace) while
|
|
7
|
+
* preserving the original bytes of every untouched line.
|
|
8
|
+
*/
|
|
9
|
+
export interface EditSpec {
|
|
10
|
+
oldText: string;
|
|
11
|
+
newText: string;
|
|
12
|
+
}
|
|
13
|
+
export interface PlannedReplacement {
|
|
14
|
+
start: number;
|
|
15
|
+
end: number;
|
|
16
|
+
replacement: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class EditError extends Error {
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Normalize the model-typical transcription slips. Applied ONLY for the fuzzy
|
|
22
|
+
* fallback — never to the bytes written back.
|
|
23
|
+
*/
|
|
24
|
+
export declare function normalizeForFuzzy(s: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Validate all edits against `content` and plan the replacements. Throws EditError
|
|
27
|
+
* with a model-actionable message on any contract violation.
|
|
28
|
+
*/
|
|
29
|
+
export declare function planEdits(content: string, edits: EditSpec[], pathLabel: string): PlannedReplacement[];
|
|
30
|
+
/** Apply planned replacements in reverse offset order (offsets stay valid). */
|
|
31
|
+
export declare function applyPlanned(content: string, planned: PlannedReplacement[]): string;
|
|
32
|
+
/**
|
|
33
|
+
* A compact unified-style preview for the approval dialog: the replaced region as
|
|
34
|
+
* `-` lines and its replacement as `+` lines, per edit. UI-only — never sent to
|
|
35
|
+
* the model (token economy).
|
|
36
|
+
*/
|
|
37
|
+
export declare function diffPreview(content: string, planned: PlannedReplacement[], maxLines?: number): string;
|
|
38
|
+
/** BOM + line-ending handling: strip for matching, restore on write-back. */
|
|
39
|
+
export declare function splitBom(raw: string): {
|
|
40
|
+
bom: string;
|
|
41
|
+
text: string;
|
|
42
|
+
};
|
|
43
|
+
/** 'crlf' when the first newline in the file is \r\n (pi: whoever appears first wins). */
|
|
44
|
+
export declare function detectLineEnding(text: string): 'crlf' | 'lf';
|
|
45
|
+
export declare function restoreLineEndings(text: string, ending: 'crlf' | 'lf'): string;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matching + application logic for edit_file (pi contract, 2026-08-21 dossier):
|
|
3
|
+
* every edit matches against the ORIGINAL file, must be found exactly once, edits
|
|
4
|
+
* must not overlap, and the combined result must differ from the original. Exact
|
|
5
|
+
* matching first; a conservative line-based fuzzy fallback absorbs the classic
|
|
6
|
+
* model slips (smart quotes, unicode dashes/spaces, trailing whitespace) while
|
|
7
|
+
* preserving the original bytes of every untouched line.
|
|
8
|
+
*/
|
|
9
|
+
export class EditError extends Error {
|
|
10
|
+
}
|
|
11
|
+
/** Positional label used in error messages: nothing for a single edit, `edits[i]` otherwise. */
|
|
12
|
+
function label(i, total) {
|
|
13
|
+
return total === 1 ? 'the text' : `edits[${i}]`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Normalize the model-typical transcription slips. Applied ONLY for the fuzzy
|
|
17
|
+
* fallback — never to the bytes written back.
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeForFuzzy(s) {
|
|
20
|
+
return s
|
|
21
|
+
.replace(/[\u2018\u2019\u201A\u201B]/g, "'")
|
|
22
|
+
.replace(/[\u201C\u201D\u201E\u201F]/g, '"')
|
|
23
|
+
.replace(/[\u2010-\u2015\u2212]/g, '-')
|
|
24
|
+
.replace(/[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g, ' ')
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((l) => l.trimEnd())
|
|
27
|
+
.join('\n');
|
|
28
|
+
}
|
|
29
|
+
function countOccurrences(haystack, needle) {
|
|
30
|
+
let n = 0;
|
|
31
|
+
for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + 1))
|
|
32
|
+
n += 1;
|
|
33
|
+
return n;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Fuzzy fallback: match `oldText` as a run of WHOLE LINES whose normalized forms
|
|
37
|
+
* equal the normalized oldText lines. Returns the character range of the matched
|
|
38
|
+
* lines in the original content for each occurrence.
|
|
39
|
+
*/
|
|
40
|
+
function fuzzyLineMatches(content, oldText) {
|
|
41
|
+
const lines = content.split('\n');
|
|
42
|
+
const target = normalizeForFuzzy(oldText).split('\n');
|
|
43
|
+
const norm = lines.map((l) => normalizeForFuzzy(l));
|
|
44
|
+
// Precompute line start offsets in the original content.
|
|
45
|
+
const starts = new Array(lines.length);
|
|
46
|
+
let off = 0;
|
|
47
|
+
for (let i = 0; i < lines.length; i++) {
|
|
48
|
+
starts[i] = off;
|
|
49
|
+
off += lines[i].length + 1; // +1 for '\n'
|
|
50
|
+
}
|
|
51
|
+
const found = [];
|
|
52
|
+
outer: for (let i = 0; i + target.length <= norm.length; i++) {
|
|
53
|
+
for (let j = 0; j < target.length; j++) {
|
|
54
|
+
if (norm[i + j] !== target[j])
|
|
55
|
+
continue outer;
|
|
56
|
+
}
|
|
57
|
+
const last = i + target.length - 1;
|
|
58
|
+
found.push({ start: starts[i], end: starts[last] + lines[last].length });
|
|
59
|
+
}
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validate all edits against `content` and plan the replacements. Throws EditError
|
|
64
|
+
* with a model-actionable message on any contract violation.
|
|
65
|
+
*/
|
|
66
|
+
export function planEdits(content, edits, pathLabel) {
|
|
67
|
+
if (edits.length === 0) {
|
|
68
|
+
throw new EditError('Edit tool input is invalid. edits must contain at least one replacement.');
|
|
69
|
+
}
|
|
70
|
+
const planned = [];
|
|
71
|
+
edits.forEach((edit, i) => {
|
|
72
|
+
const who = label(i, edits.length);
|
|
73
|
+
if (edit.oldText.length === 0) {
|
|
74
|
+
const field = edits.length === 1 ? 'oldText' : `edits[${i}].oldText`;
|
|
75
|
+
throw new EditError(`${field} must not be empty in ${pathLabel}.`);
|
|
76
|
+
}
|
|
77
|
+
const exact = countOccurrences(content, edit.oldText);
|
|
78
|
+
if (exact === 1) {
|
|
79
|
+
const start = content.indexOf(edit.oldText);
|
|
80
|
+
planned.push({ start, end: start + edit.oldText.length, replacement: edit.newText });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (exact > 1) {
|
|
84
|
+
throw new EditError(`Found ${exact} occurrences of ${who} in ${pathLabel}. Each oldText must be unique. ` +
|
|
85
|
+
'Please provide more context to make it unique.');
|
|
86
|
+
}
|
|
87
|
+
// Exact miss — conservative fuzzy fallback on whole-line runs.
|
|
88
|
+
const fuzzy = fuzzyLineMatches(content, edit.oldText);
|
|
89
|
+
if (fuzzy.length === 1) {
|
|
90
|
+
planned.push({ ...fuzzy[0], replacement: edit.newText });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (fuzzy.length > 1) {
|
|
94
|
+
throw new EditError(`Found ${fuzzy.length} occurrences of ${who} in ${pathLabel}. Each oldText must be unique. ` +
|
|
95
|
+
'Please provide more context to make it unique.');
|
|
96
|
+
}
|
|
97
|
+
throw new EditError(`Could not find ${who} in ${pathLabel}. The oldText must match exactly, including all ` +
|
|
98
|
+
'whitespace and newlines. Note: read_file output prefixes each line with a line number and a ' +
|
|
99
|
+
'tab — never include that prefix in oldText.');
|
|
100
|
+
});
|
|
101
|
+
// Overlap check on the ORIGINAL-file ranges.
|
|
102
|
+
const order = planned.map((p, i) => ({ ...p, i })).sort((a, b) => a.start - b.start);
|
|
103
|
+
for (let k = 1; k < order.length; k++) {
|
|
104
|
+
if (order[k].start < order[k - 1].end) {
|
|
105
|
+
throw new EditError(`edits[${order[k - 1].i}] and edits[${order[k].i}] overlap in ${pathLabel}. ` +
|
|
106
|
+
'Merge them into one edit or target disjoint regions.');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return planned;
|
|
110
|
+
}
|
|
111
|
+
/** Apply planned replacements in reverse offset order (offsets stay valid). */
|
|
112
|
+
export function applyPlanned(content, planned) {
|
|
113
|
+
const byStart = [...planned].sort((a, b) => b.start - a.start);
|
|
114
|
+
let out = content;
|
|
115
|
+
for (const p of byStart) {
|
|
116
|
+
out = out.slice(0, p.start) + p.replacement + out.slice(p.end);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A compact unified-style preview for the approval dialog: the replaced region as
|
|
122
|
+
* `-` lines and its replacement as `+` lines, per edit. UI-only — never sent to
|
|
123
|
+
* the model (token economy).
|
|
124
|
+
*/
|
|
125
|
+
export function diffPreview(content, planned, maxLines = 60) {
|
|
126
|
+
const parts = [];
|
|
127
|
+
const inOrder = [...planned].sort((a, b) => a.start - b.start);
|
|
128
|
+
for (const p of inOrder) {
|
|
129
|
+
const oldPart = content.slice(p.start, p.end);
|
|
130
|
+
for (const l of oldPart.split('\n'))
|
|
131
|
+
parts.push(`- ${l}`);
|
|
132
|
+
for (const l of p.replacement.split('\n'))
|
|
133
|
+
parts.push(`+ ${l}`);
|
|
134
|
+
parts.push('');
|
|
135
|
+
}
|
|
136
|
+
if (parts.at(-1) === '')
|
|
137
|
+
parts.pop();
|
|
138
|
+
if (parts.length > maxLines) {
|
|
139
|
+
const hidden = parts.length - maxLines;
|
|
140
|
+
return parts.slice(0, maxLines).join('\n') + `\n… (+${hidden} more lines)`;
|
|
141
|
+
}
|
|
142
|
+
return parts.join('\n');
|
|
143
|
+
}
|
|
144
|
+
/** BOM + line-ending handling: strip for matching, restore on write-back. */
|
|
145
|
+
export function splitBom(raw) {
|
|
146
|
+
return raw.startsWith('\uFEFF') ? { bom: '\uFEFF', text: raw.slice(1) } : { bom: '', text: raw };
|
|
147
|
+
}
|
|
148
|
+
/** 'crlf' when the first newline in the file is \r\n (pi: whoever appears first wins). */
|
|
149
|
+
export function detectLineEnding(text) {
|
|
150
|
+
const i = text.indexOf('\n');
|
|
151
|
+
return i > 0 && text[i - 1] === '\r' ? 'crlf' : 'lf';
|
|
152
|
+
}
|
|
153
|
+
export function restoreLineEndings(text, ending) {
|
|
154
|
+
return ending === 'crlf' ? text.replace(/\n/g, '\r\n') : text;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=editDiff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editDiff.js","sourceRoot":"","sources":["../../../src/coding/editDiff.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ClientTool } from '../core/types.js';
|
|
2
|
+
import type { PermissionGate } from './permissions.js';
|
|
3
|
+
import type { ReadFileState } from './readState.js';
|
|
4
|
+
/**
|
|
5
|
+
* The `edit_file` tool: exact-text replacement with a pi-contract edits[] array.
|
|
6
|
+
* All edits match against the ORIGINAL file, each must be unique and non-overlapping,
|
|
7
|
+
* and the file must have been read this session (and be unchanged since). BOM and
|
|
8
|
+
* CRLF line endings are preserved on write-back.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createEditFileTool(opts: {
|
|
11
|
+
root: string;
|
|
12
|
+
gate: PermissionGate;
|
|
13
|
+
readState: ReadFileState;
|
|
14
|
+
}): ClientTool;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { GuardError, resolveInWorkspace, sanitizeFsError } from '../fs/workspaceGuard.js';
|
|
3
|
+
import { EditError, applyPlanned, detectLineEnding, diffPreview, planEdits, restoreLineEndings, splitBom, } from './editDiff.js';
|
|
4
|
+
const MAX_EDIT_FILE_SIZE_BYTES = 8 * 1024 * 1024; // matches the read tools' range-read ceiling
|
|
5
|
+
/**
|
|
6
|
+
* The `edit_file` tool: exact-text replacement with a pi-contract edits[] array.
|
|
7
|
+
* All edits match against the ORIGINAL file, each must be unique and non-overlapping,
|
|
8
|
+
* and the file must have been read this session (and be unchanged since). BOM and
|
|
9
|
+
* CRLF line endings are preserved on write-back.
|
|
10
|
+
*/
|
|
11
|
+
export function createEditFileTool(opts) {
|
|
12
|
+
return {
|
|
13
|
+
name: 'edit_file',
|
|
14
|
+
readOnly: false,
|
|
15
|
+
description: 'Edit a single file using exact text replacement. Every edits[].old_text must match a unique, ' +
|
|
16
|
+
'non-overlapping region of the original file (match includes whitespace and newlines; never include ' +
|
|
17
|
+
'the line-number prefix from read_file output). When changing multiple separate locations in one ' +
|
|
18
|
+
'file, use ONE call with multiple entries in edits[]. Keep each old_text as small as possible while ' +
|
|
19
|
+
'still unique. The file must have been read this session before editing.',
|
|
20
|
+
parameters: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
path: { type: 'string', description: 'File path relative to the workspace root.' },
|
|
24
|
+
edits: {
|
|
25
|
+
type: 'array',
|
|
26
|
+
description: 'One or more targeted replacements. Each is matched against the original file, not ' +
|
|
27
|
+
'incrementally. Merge nearby changes into one edit instead of overlapping edits.',
|
|
28
|
+
items: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
old_text: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Exact existing text; must be unique in the file.',
|
|
34
|
+
},
|
|
35
|
+
new_text: { type: 'string', description: 'Replacement text.' },
|
|
36
|
+
},
|
|
37
|
+
required: ['old_text', 'new_text'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: ['path', 'edits'],
|
|
42
|
+
},
|
|
43
|
+
async execute(args) {
|
|
44
|
+
if (typeof args.path !== 'string' || !args.path.trim()) {
|
|
45
|
+
return { error: 'Missing required "path" argument.' };
|
|
46
|
+
}
|
|
47
|
+
const edits = coerceEdits(args);
|
|
48
|
+
if (typeof edits === 'string')
|
|
49
|
+
return { error: edits };
|
|
50
|
+
try {
|
|
51
|
+
const file = await resolveInWorkspace(opts.root, args.path);
|
|
52
|
+
const stat = await fs.stat(file);
|
|
53
|
+
if (stat.isDirectory())
|
|
54
|
+
return { error: `Is a directory, not a file: ${args.path}` };
|
|
55
|
+
if (!stat.isFile())
|
|
56
|
+
return { error: `Not a regular file: ${args.path}` };
|
|
57
|
+
if (stat.size > MAX_EDIT_FILE_SIZE_BYTES) {
|
|
58
|
+
return { error: `File is ${stat.size} bytes (>8MB) — too large to edit.` };
|
|
59
|
+
}
|
|
60
|
+
if (!opts.readState.has(file)) {
|
|
61
|
+
return {
|
|
62
|
+
error: `File has not been read yet: ${args.path}. Read it first before editing it.`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (opts.readState.isStale(file, stat.mtimeMs)) {
|
|
66
|
+
return {
|
|
67
|
+
error: `File has been modified since read, either by the user or by another tool: ${args.path}. Read it again before editing it.`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const raw = await fs.readFile(file, 'utf8');
|
|
71
|
+
const { bom, text } = splitBom(raw);
|
|
72
|
+
const ending = detectLineEnding(text);
|
|
73
|
+
const content = ending === 'crlf' ? text.replace(/\r\n/g, '\n') : text;
|
|
74
|
+
// Models virtually always transcribe LF; normalize the specs the same way.
|
|
75
|
+
const specs = edits.map((e) => ({
|
|
76
|
+
oldText: e.oldText.replace(/\r\n/g, '\n'),
|
|
77
|
+
newText: e.newText.replace(/\r\n/g, '\n'),
|
|
78
|
+
}));
|
|
79
|
+
const planned = planEdits(content, specs, args.path);
|
|
80
|
+
const updated = applyPlanned(content, planned);
|
|
81
|
+
if (updated === content) {
|
|
82
|
+
return {
|
|
83
|
+
error: `No changes made to ${args.path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const verdict = await opts.gate.check({
|
|
87
|
+
kind: 'edit',
|
|
88
|
+
path: file,
|
|
89
|
+
preview: diffPreview(content, planned),
|
|
90
|
+
});
|
|
91
|
+
if (!verdict.ok)
|
|
92
|
+
return { error: verdict.message };
|
|
93
|
+
// Re-stat right before writing: the approval dialog can stay open for a while,
|
|
94
|
+
// and a file changed underneath must not be clobbered with a stale base.
|
|
95
|
+
const afterStat = await fs.stat(file);
|
|
96
|
+
if (afterStat.mtimeMs !== stat.mtimeMs) {
|
|
97
|
+
return {
|
|
98
|
+
error: `File has been modified since read, either by the user or by another tool: ${args.path}. Read it again before editing it.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
await fs.writeFile(file, bom + restoreLineEndings(updated, ending), 'utf8');
|
|
102
|
+
const newStat = await fs.stat(file);
|
|
103
|
+
opts.readState.record(file, newStat.mtimeMs);
|
|
104
|
+
return { output: `Successfully replaced ${planned.length} block(s) in ${args.path}.` };
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
if (err instanceof EditError)
|
|
108
|
+
return { error: err.message };
|
|
109
|
+
if (err instanceof GuardError)
|
|
110
|
+
return { error: err.message };
|
|
111
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
112
|
+
return { error: `edit_file failed: ${sanitizeFsError(message, opts.root)}` };
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Tolerant argument coercion (pi prepareArguments): models sometimes send `edits`
|
|
119
|
+
* as a JSON string, a single object instead of a one-element array, or legacy
|
|
120
|
+
* top-level old_text/new_text. Returns the normalized list or an error string.
|
|
121
|
+
*/
|
|
122
|
+
function coerceEdits(args) {
|
|
123
|
+
let rawEdits = args.edits;
|
|
124
|
+
if (typeof rawEdits === 'string') {
|
|
125
|
+
try {
|
|
126
|
+
rawEdits = JSON.parse(rawEdits);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return 'Invalid "edits": not valid JSON.';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (rawEdits && !Array.isArray(rawEdits) && typeof rawEdits === 'object') {
|
|
133
|
+
rawEdits = [rawEdits];
|
|
134
|
+
}
|
|
135
|
+
const list = Array.isArray(rawEdits) ? [...rawEdits] : [];
|
|
136
|
+
if (typeof args.old_text === 'string' && typeof args.new_text === 'string') {
|
|
137
|
+
list.push({ old_text: args.old_text, new_text: args.new_text });
|
|
138
|
+
}
|
|
139
|
+
if (list.length === 0) {
|
|
140
|
+
return 'Edit tool input is invalid. edits must contain at least one replacement.';
|
|
141
|
+
}
|
|
142
|
+
const specs = [];
|
|
143
|
+
for (const item of list) {
|
|
144
|
+
const o = item;
|
|
145
|
+
const oldText = typeof o.old_text === 'string' ? o.old_text : typeof o.oldText === 'string' ? o.oldText : null;
|
|
146
|
+
const newText = typeof o.new_text === 'string' ? o.new_text : typeof o.newText === 'string' ? o.newText : null;
|
|
147
|
+
if (oldText === null || newText === null) {
|
|
148
|
+
return 'Each edit needs string old_text and new_text.';
|
|
149
|
+
}
|
|
150
|
+
specs.push({ oldText, newText });
|
|
151
|
+
}
|
|
152
|
+
return specs;
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=editTool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editTool.js","sourceRoot":"","sources":["../../../src/coding/editTool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Public surface of the coding tools (bash / write_file / edit_file), 2026-08-21 spec. */
|
|
2
|
+
import type { ClientTool } from '../core/types.js';
|
|
3
|
+
import type { BackgroundBashTasks } from './backgroundTasks.js';
|
|
4
|
+
import type { PermissionGate } from './permissions.js';
|
|
5
|
+
import type { ReadFileState } from './readState.js';
|
|
6
|
+
export { ReadFileState } from './readState.js';
|
|
7
|
+
export { CodingPermissionGate, type PermissionMode, type Approver, type ApprovalRequest, type ApprovalDecision, } from './permissionGate.js';
|
|
8
|
+
export { allowAllGate, type PermissionGate, type CodingAction, type GateVerdict } from './permissions.js';
|
|
9
|
+
export { buildCodingSystemMessage, loadProjectContext } from './projectContext.js';
|
|
10
|
+
export { BackgroundBashTasks } from './backgroundTasks.js';
|
|
11
|
+
/** The coding tool names — used by surfaces that need to filter or report on them. */
|
|
12
|
+
export declare const CODING_TOOL_NAMES: ReadonlySet<string>;
|
|
13
|
+
/**
|
|
14
|
+
* The three mutating tools, jailed to `root` (write/edit; bash runs with cwd=root)
|
|
15
|
+
* and gated by `gate`. Registration order = model-visible order.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createCodingTools(opts: {
|
|
18
|
+
root: string;
|
|
19
|
+
gate: PermissionGate;
|
|
20
|
+
readState: ReadFileState;
|
|
21
|
+
/** Background bash registry (caller owns lifecycle/cleanup); absent = no background tools. */
|
|
22
|
+
background?: BackgroundBashTasks;
|
|
23
|
+
/** Test hook: where bash spill files go. */
|
|
24
|
+
spillDir?: string;
|
|
25
|
+
}): ClientTool[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createBashTool } from './bashTool.js';
|
|
2
|
+
import { createWriteFileTool } from './writeTool.js';
|
|
3
|
+
import { createEditFileTool } from './editTool.js';
|
|
4
|
+
import { createBashOutputTool, createBashKillTool } from './backgroundTools.js';
|
|
5
|
+
export { ReadFileState } from './readState.js';
|
|
6
|
+
export { CodingPermissionGate, } from './permissionGate.js';
|
|
7
|
+
export { allowAllGate } from './permissions.js';
|
|
8
|
+
export { buildCodingSystemMessage, loadProjectContext } from './projectContext.js';
|
|
9
|
+
export { BackgroundBashTasks } from './backgroundTasks.js';
|
|
10
|
+
/** The coding tool names — used by surfaces that need to filter or report on them. */
|
|
11
|
+
export const CODING_TOOL_NAMES = new Set([
|
|
12
|
+
'bash',
|
|
13
|
+
'write_file',
|
|
14
|
+
'edit_file',
|
|
15
|
+
'bash_output',
|
|
16
|
+
'bash_kill',
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* The three mutating tools, jailed to `root` (write/edit; bash runs with cwd=root)
|
|
20
|
+
* and gated by `gate`. Registration order = model-visible order.
|
|
21
|
+
*/
|
|
22
|
+
export function createCodingTools(opts) {
|
|
23
|
+
return [
|
|
24
|
+
createBashTool({ cwd: opts.root, gate: opts.gate, background: opts.background, spillDir: opts.spillDir }),
|
|
25
|
+
createWriteFileTool({ root: opts.root, gate: opts.gate, readState: opts.readState }),
|
|
26
|
+
createEditFileTool({ root: opts.root, gate: opts.gate, readState: opts.readState }),
|
|
27
|
+
...(opts.background ? [createBashOutputTool(opts.background), createBashKillTool(opts.background)] : []),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=index.js.map
|