@oh-my-pi/pi-coding-agent 14.1.2 → 14.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/CHANGELOG.md +47 -2
- package/package.json +8 -8
- package/scripts/build-binary.ts +61 -0
- package/src/autoresearch/helpers.ts +10 -0
- package/src/autoresearch/index.ts +1 -11
- package/src/autoresearch/tools/init-experiment.ts +1 -10
- package/src/autoresearch/tools/log-experiment.ts +1 -11
- package/src/autoresearch/tools/run-experiment.ts +1 -10
- package/src/bun-imports.d.ts +6 -0
- package/src/cli/plugin-cli.ts +23 -45
- package/src/commit/agentic/tools/propose-commit.ts +1 -14
- package/src/commit/agentic/tools/split-commit.ts +1 -15
- package/src/commit/utils.ts +15 -1
- package/src/config/model-registry.ts +3 -3
- package/src/config/prompt-templates.ts +4 -12
- package/src/config/settings-schema.ts +27 -2
- package/src/config/settings.ts +1 -1
- package/src/dap/session.ts +8 -2
- package/src/discovery/claude-plugins.ts +61 -6
- package/src/discovery/codex.ts +2 -15
- package/src/discovery/gemini.ts +2 -15
- package/src/discovery/helpers.ts +40 -1
- package/src/discovery/opencode.ts +2 -15
- package/src/edit/apply-patch/index.ts +87 -0
- package/src/edit/apply-patch/parser.ts +174 -0
- package/src/edit/diff.ts +3 -14
- package/src/edit/index.ts +67 -3
- package/src/edit/modes/apply-patch.lark +19 -0
- package/src/edit/modes/apply-patch.ts +63 -0
- package/src/edit/modes/chunk.ts +6 -2
- package/src/edit/modes/hashline.ts +3 -3
- package/src/edit/modes/replace.ts +2 -13
- package/src/edit/read-file.ts +18 -0
- package/src/edit/renderer.ts +61 -33
- package/src/extensibility/extensions/compact-handler.ts +40 -0
- package/src/extensibility/extensions/runner.ts +11 -29
- package/src/extensibility/utils.ts +7 -1
- package/src/internal-urls/docs-index.generated.ts +9 -2
- package/src/lsp/client.ts +14 -5
- package/src/lsp/index.ts +53 -10
- package/src/lsp/render.ts +14 -2
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +1 -0
- package/src/mcp/manager.ts +29 -48
- package/src/memories/index.ts +7 -1
- package/src/modes/acp/acp-agent.ts +3 -16
- package/src/modes/components/model-selector.ts +15 -24
- package/src/modes/components/plugin-settings.ts +16 -5
- package/src/modes/components/read-tool-group.ts +92 -9
- package/src/modes/components/settings-defs.ts +18 -0
- package/src/modes/components/settings-selector.ts +2 -6
- package/src/modes/components/tool-execution.ts +61 -28
- package/src/modes/controllers/event-controller.ts +3 -1
- package/src/modes/controllers/extension-ui-controller.ts +99 -150
- package/src/modes/controllers/selector-controller.ts +3 -12
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/print-mode.ts +4 -22
- package/src/modes/rpc/rpc-mode.ts +18 -38
- package/src/modes/shared.ts +10 -1
- package/src/modes/utils/ui-helpers.ts +6 -2
- package/src/plan-mode/approved-plan.ts +5 -4
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/subagent-user-prompt.md +2 -2
- package/src/prompts/system/system-prompt.md +208 -243
- package/src/prompts/tools/apply-patch.md +67 -0
- package/src/prompts/tools/ast-edit.md +18 -23
- package/src/prompts/tools/ast-grep.md +25 -32
- package/src/prompts/tools/bash.md +11 -23
- package/src/prompts/tools/debug.md +8 -22
- package/src/prompts/tools/find.md +0 -4
- package/src/prompts/tools/grep.md +3 -5
- package/src/prompts/tools/hashline.md +16 -10
- package/src/prompts/tools/python.md +10 -14
- package/src/prompts/tools/read.md +17 -24
- package/src/prompts/tools/task.md +57 -21
- package/src/prompts/tools/todo-write.md +45 -67
- package/src/session/agent-session.ts +4 -4
- package/src/session/session-manager.ts +15 -7
- package/src/session/streaming-output.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +3 -14
- package/src/task/executor.ts +13 -34
- package/src/task/index.ts +82 -18
- package/src/task/simple-mode.ts +27 -0
- package/src/task/template.ts +17 -3
- package/src/task/types.ts +77 -30
- package/src/tools/ask.ts +2 -4
- package/src/tools/ast-edit.ts +41 -17
- package/src/tools/ast-grep.ts +8 -27
- package/src/tools/bash-skill-urls.ts +9 -7
- package/src/tools/bash.ts +66 -24
- package/src/tools/browser.ts +1 -1
- package/src/tools/fetch.ts +1 -14
- package/src/tools/file-recorder.ts +35 -0
- package/src/tools/find.ts +25 -29
- package/src/tools/gh-format.ts +12 -0
- package/src/tools/gh-renderer.ts +1 -8
- package/src/tools/gh.ts +6 -13
- package/src/tools/grep.ts +103 -59
- package/src/tools/jtd-to-json-schema.ts +16 -0
- package/src/tools/match-line-format.ts +20 -0
- package/src/tools/path-utils.ts +61 -5
- package/src/tools/plan-mode-guard.ts +6 -5
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +38 -6
- package/src/tools/renderers.ts +1 -0
- package/src/tools/resolve.ts +12 -3
- package/src/tools/ssh.ts +3 -11
- package/src/tools/submit-result.ts +1 -13
- package/src/tools/todo-write.ts +137 -103
- package/src/tools/vim.ts +1 -1
- package/src/tools/write.ts +2 -23
- package/src/tui/code-cell.ts +12 -7
- package/src/utils/edit-mode.ts +3 -2
- package/src/utils/git.ts +1 -1
- package/src/vim/engine.ts +41 -58
- package/src/web/scrapers/crates-io.ts +1 -14
- package/src/web/scrapers/types.ts +13 -0
- package/src/web/search/providers/base.ts +13 -0
- package/src/web/search/providers/brave.ts +2 -5
- package/src/web/search/providers/codex.ts +20 -24
- package/src/web/search/providers/gemini.ts +39 -1
- package/src/web/search/providers/jina.ts +2 -5
- package/src/web/search/providers/kagi.ts +3 -8
- package/src/web/search/providers/kimi.ts +3 -7
- package/src/web/search/providers/parallel.ts +3 -8
- package/src/web/search/providers/synthetic.ts +3 -7
- package/src/web/search/providers/tavily.ts +15 -11
- package/src/web/search/providers/utils.ts +36 -0
- package/src/web/search/providers/zai.ts +3 -7
|
@@ -28,6 +28,58 @@ const PROVIDER_ID = "claude-plugins";
|
|
|
28
28
|
const DISPLAY_NAME = "Claude Code Marketplace";
|
|
29
29
|
const PRIORITY = 70; // Below claude.ts (80) so user .claude/ overrides win
|
|
30
30
|
|
|
31
|
+
interface ClaudePluginManifest {
|
|
32
|
+
skills?: string;
|
|
33
|
+
"slash-commands"?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ResolvedPluginDir {
|
|
37
|
+
dir: string;
|
|
38
|
+
warning?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readPluginManifest(root: ClaudePluginRoot): Promise<ClaudePluginManifest | null> {
|
|
42
|
+
const manifestPath = path.join(root.path, ".claude-plugin", "plugin.json");
|
|
43
|
+
const raw = await readFile(manifestPath);
|
|
44
|
+
if (raw === null) return null;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
49
|
+
return parsed as ClaudePluginManifest;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isWithinPluginRoot(rootPath: string, targetPath: string): boolean {
|
|
56
|
+
const relative = path.relative(rootPath, targetPath);
|
|
57
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function resolvePluginDir(
|
|
61
|
+
root: ClaudePluginRoot,
|
|
62
|
+
manifestKey: keyof ClaudePluginManifest,
|
|
63
|
+
fallback: string,
|
|
64
|
+
): Promise<ResolvedPluginDir> {
|
|
65
|
+
const manifest = await readPluginManifest(root);
|
|
66
|
+
const fallbackDir = path.join(root.path, fallback);
|
|
67
|
+
const configured = manifest?.[manifestKey];
|
|
68
|
+
if (typeof configured !== "string" || !configured.trim()) {
|
|
69
|
+
return { dir: fallbackDir };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const resolved = path.resolve(root.path, configured.trim());
|
|
73
|
+
if (isWithinPluginRoot(root.path, resolved)) {
|
|
74
|
+
return { dir: resolved };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
dir: fallbackDir,
|
|
79
|
+
warning: `[claude-plugins] Ignoring ${String(manifestKey)} path outside plugin root for ${root.id}: ${configured}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
31
83
|
// =============================================================================
|
|
32
84
|
// Skills
|
|
33
85
|
// =============================================================================
|
|
@@ -41,17 +93,18 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
|
41
93
|
|
|
42
94
|
const results = await Promise.all(
|
|
43
95
|
roots.map(async root => {
|
|
44
|
-
const skillsDir =
|
|
96
|
+
const { dir: skillsDir, warning } = await resolvePluginDir(root, "skills", "skills");
|
|
45
97
|
const result = await scanSkillsFromDir(ctx, {
|
|
46
98
|
dir: skillsDir,
|
|
47
99
|
providerId: PROVIDER_ID,
|
|
48
100
|
level: root.scope,
|
|
49
101
|
});
|
|
50
|
-
return { root, result };
|
|
102
|
+
return { root, result, warning };
|
|
51
103
|
}),
|
|
52
104
|
);
|
|
53
105
|
|
|
54
|
-
for (const { root, result } of results) {
|
|
106
|
+
for (const { root, result, warning } of results) {
|
|
107
|
+
if (warning) warnings.push(warning);
|
|
55
108
|
for (const skill of result.items) {
|
|
56
109
|
if (root.plugin) skill.name = `${root.plugin}:${skill.name}`;
|
|
57
110
|
items.push(skill);
|
|
@@ -75,8 +128,8 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
|
|
|
75
128
|
|
|
76
129
|
const results = await Promise.all(
|
|
77
130
|
roots.map(async root => {
|
|
78
|
-
const commandsDir =
|
|
79
|
-
|
|
131
|
+
const { dir: commandsDir, warning } = await resolvePluginDir(root, "slash-commands", "commands");
|
|
132
|
+
const result = await loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, root.scope, {
|
|
80
133
|
extensions: ["md"],
|
|
81
134
|
transform: (name, content, filePath, source) => {
|
|
82
135
|
const cmdName = name.replace(/\.md$/, "");
|
|
@@ -89,10 +142,12 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
|
|
|
89
142
|
};
|
|
90
143
|
},
|
|
91
144
|
});
|
|
145
|
+
return { result, warning };
|
|
92
146
|
}),
|
|
93
147
|
);
|
|
94
148
|
|
|
95
|
-
for (const result of results) {
|
|
149
|
+
for (const { result, warning } of results) {
|
|
150
|
+
if (warning) warnings.push(warning);
|
|
96
151
|
items.push(...result.items);
|
|
97
152
|
if (result.warnings) warnings.push(...result.warnings);
|
|
98
153
|
}
|
package/src/discovery/codex.ts
CHANGED
|
@@ -30,9 +30,9 @@ import { toolCapability } from "../capability/tool";
|
|
|
30
30
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
31
31
|
|
|
32
32
|
import {
|
|
33
|
+
buildExtensionModuleItems,
|
|
33
34
|
createSourceMeta,
|
|
34
35
|
discoverExtensionModulePaths,
|
|
35
|
-
getExtensionNameFromPath,
|
|
36
36
|
loadFilesFromDir,
|
|
37
37
|
SOURCE_PATHS,
|
|
38
38
|
scanSkillsFromDir,
|
|
@@ -251,20 +251,7 @@ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<Extens
|
|
|
251
251
|
discoverExtensionModulePaths(ctx, projectExtensionsDir),
|
|
252
252
|
]);
|
|
253
253
|
|
|
254
|
-
const items
|
|
255
|
-
...userPaths.map(extPath => ({
|
|
256
|
-
name: getExtensionNameFromPath(extPath),
|
|
257
|
-
path: extPath,
|
|
258
|
-
level: "user" as const,
|
|
259
|
-
_source: createSourceMeta(PROVIDER_ID, extPath, "user"),
|
|
260
|
-
})),
|
|
261
|
-
...projectPaths.map(extPath => ({
|
|
262
|
-
name: getExtensionNameFromPath(extPath),
|
|
263
|
-
path: extPath,
|
|
264
|
-
level: "project" as const,
|
|
265
|
-
_source: createSourceMeta(PROVIDER_ID, extPath, "project"),
|
|
266
|
-
})),
|
|
267
|
-
];
|
|
254
|
+
const items = buildExtensionModuleItems(PROVIDER_ID, userPaths, projectPaths);
|
|
268
255
|
|
|
269
256
|
return { items, warnings };
|
|
270
257
|
}
|
package/src/discovery/gemini.ts
CHANGED
|
@@ -27,11 +27,11 @@ import { type Settings, settingsCapability } from "../capability/settings";
|
|
|
27
27
|
import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
|
|
28
28
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
29
29
|
import {
|
|
30
|
+
buildExtensionModuleItems,
|
|
30
31
|
calculateDepth,
|
|
31
32
|
createSourceMeta,
|
|
32
33
|
discoverExtensionModulePaths,
|
|
33
34
|
expandEnvVarsDeep,
|
|
34
|
-
getExtensionNameFromPath,
|
|
35
35
|
getProjectPath,
|
|
36
36
|
getUserPath,
|
|
37
37
|
} from "./helpers";
|
|
@@ -238,20 +238,7 @@ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<Extens
|
|
|
238
238
|
projectExtensionsDir ? discoverExtensionModulePaths(ctx, projectExtensionsDir) : Promise.resolve([]),
|
|
239
239
|
]);
|
|
240
240
|
|
|
241
|
-
const items
|
|
242
|
-
...userPaths.map(extPath => ({
|
|
243
|
-
name: getExtensionNameFromPath(extPath),
|
|
244
|
-
path: extPath,
|
|
245
|
-
level: "user" as const,
|
|
246
|
-
_source: createSourceMeta(PROVIDER_ID, extPath, "user"),
|
|
247
|
-
})),
|
|
248
|
-
...projectPaths.map(extPath => ({
|
|
249
|
-
name: getExtensionNameFromPath(extPath),
|
|
250
|
-
path: extPath,
|
|
251
|
-
level: "project" as const,
|
|
252
|
-
_source: createSourceMeta(PROVIDER_ID, extPath, "project"),
|
|
253
|
-
})),
|
|
254
|
-
];
|
|
241
|
+
const items = buildExtensionModuleItems(PROVIDER_ID, userPaths, projectPaths);
|
|
255
242
|
|
|
256
243
|
return { items, warnings: [] };
|
|
257
244
|
}
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -4,7 +4,8 @@ import * as path from "node:path";
|
|
|
4
4
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import { FileType, glob } from "@oh-my-pi/pi-natives";
|
|
6
6
|
import { CONFIG_DIR_NAME, getConfigDirName, getProjectDir, parseFrontmatter, tryParseJson } from "@oh-my-pi/pi-utils";
|
|
7
|
-
import {
|
|
7
|
+
import type { ExtensionModule } from "../capability/extension-module";
|
|
8
|
+
import { invalidate as invalidateFsCache, readDirEntries, readFile } from "../capability/fs";
|
|
8
9
|
import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
|
|
9
10
|
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
|
10
11
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
@@ -581,6 +582,31 @@ export function getExtensionNameFromPath(extensionPath: string): string {
|
|
|
581
582
|
return base;
|
|
582
583
|
}
|
|
583
584
|
|
|
585
|
+
/**
|
|
586
|
+
* Build ExtensionModule items from discovered user/project paths.
|
|
587
|
+
* Shared across providers that expose extension modules via user + project dirs.
|
|
588
|
+
*/
|
|
589
|
+
export function buildExtensionModuleItems(
|
|
590
|
+
providerId: string,
|
|
591
|
+
userPaths: string[],
|
|
592
|
+
projectPaths: string[],
|
|
593
|
+
): ExtensionModule[] {
|
|
594
|
+
return [
|
|
595
|
+
...userPaths.map(extPath => ({
|
|
596
|
+
name: getExtensionNameFromPath(extPath),
|
|
597
|
+
path: extPath,
|
|
598
|
+
level: "user" as const,
|
|
599
|
+
_source: createSourceMeta(providerId, extPath, "user"),
|
|
600
|
+
})),
|
|
601
|
+
...projectPaths.map(extPath => ({
|
|
602
|
+
name: getExtensionNameFromPath(extPath),
|
|
603
|
+
path: extPath,
|
|
604
|
+
level: "project" as const,
|
|
605
|
+
_source: createSourceMeta(providerId, extPath, "project"),
|
|
606
|
+
})),
|
|
607
|
+
];
|
|
608
|
+
}
|
|
609
|
+
|
|
584
610
|
// =============================================================================
|
|
585
611
|
// Claude Code Plugin Cache Helpers
|
|
586
612
|
// =============================================================================
|
|
@@ -896,6 +922,19 @@ export function clearClaudePluginRootsCache(): void {
|
|
|
896
922
|
}
|
|
897
923
|
}
|
|
898
924
|
|
|
925
|
+
/**
|
|
926
|
+
* Invalidate fs caches for installed-plugin registry files and reset the
|
|
927
|
+
* in-memory plugin roots cache. Used by MarketplaceManager clients after
|
|
928
|
+
* installing/uninstalling/enabling/disabling plugins.
|
|
929
|
+
*/
|
|
930
|
+
export function clearPluginRootsAndCaches(extraPaths?: readonly string[]): void {
|
|
931
|
+
const home = os.homedir();
|
|
932
|
+
invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
|
|
933
|
+
invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
|
|
934
|
+
for (const p of extraPaths ?? []) invalidateFsCache(p);
|
|
935
|
+
clearClaudePluginRootsCache();
|
|
936
|
+
}
|
|
937
|
+
|
|
899
938
|
// ── Preloaded plugin roots (for sync consumers like LSP config) ─────────────
|
|
900
939
|
// Populated at startup by preloadPluginRoots(). Read synchronously by
|
|
901
940
|
// getPreloadedPluginRoots(). Safe degradation: empty array if not warmed.
|
|
@@ -28,10 +28,10 @@ import { type SlashCommand, slashCommandCapability } from "../capability/slash-c
|
|
|
28
28
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
29
29
|
|
|
30
30
|
import {
|
|
31
|
+
buildExtensionModuleItems,
|
|
31
32
|
createSourceMeta,
|
|
32
33
|
discoverExtensionModulePaths,
|
|
33
34
|
expandEnvVarsDeep,
|
|
34
|
-
getExtensionNameFromPath,
|
|
35
35
|
getProjectPath,
|
|
36
36
|
getUserPath,
|
|
37
37
|
loadFilesFromDir,
|
|
@@ -227,20 +227,7 @@ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<Extens
|
|
|
227
227
|
projectPluginsDir ? discoverExtensionModulePaths(ctx, projectPluginsDir) : Promise.resolve([]),
|
|
228
228
|
]);
|
|
229
229
|
|
|
230
|
-
const items
|
|
231
|
-
...userPaths.map(extPath => ({
|
|
232
|
-
name: getExtensionNameFromPath(extPath),
|
|
233
|
-
path: extPath,
|
|
234
|
-
level: "user" as const,
|
|
235
|
-
_source: createSourceMeta(PROVIDER_ID, extPath, "user"),
|
|
236
|
-
})),
|
|
237
|
-
...projectPaths.map(extPath => ({
|
|
238
|
-
name: getExtensionNameFromPath(extPath),
|
|
239
|
-
path: extPath,
|
|
240
|
-
level: "project" as const,
|
|
241
|
-
_source: createSourceMeta(PROVIDER_ID, extPath, "project"),
|
|
242
|
-
})),
|
|
243
|
-
];
|
|
230
|
+
const items = buildExtensionModuleItems(PROVIDER_ID, userPaths, projectPaths);
|
|
244
231
|
|
|
245
232
|
return { items, warnings: [] };
|
|
246
233
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-file orchestrator for the Codex `apply_patch` envelope.
|
|
3
|
+
*
|
|
4
|
+
* Decoupled from tool-registration: takes raw patch text + options, parses
|
|
5
|
+
* it, and applies each hunk via the existing single-file `applyPatch` in
|
|
6
|
+
* `../modes/patch.ts`. A future OpenAI freeform/grammar tool variant can
|
|
7
|
+
* call this directly with the raw grammar output.
|
|
8
|
+
*
|
|
9
|
+
* Per spec §6.1, hunks are applied in order and NOT atomically — if hunk
|
|
10
|
+
* N fails, hunks `0..N-1` are already on disk. We surface that by
|
|
11
|
+
* returning the per-file results alongside the error when it happens.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { ApplyPatchError } from "../diff";
|
|
15
|
+
import { type ApplyPatchOptions, type ApplyPatchResult, applyPatch, type PatchInput } from "../modes/patch";
|
|
16
|
+
import { parseApplyPatch } from "./parser";
|
|
17
|
+
|
|
18
|
+
export * from "./parser";
|
|
19
|
+
|
|
20
|
+
export interface ApplyCodexPatchResult {
|
|
21
|
+
/** Single-file apply results in the order they were attempted. */
|
|
22
|
+
results: ApplyPatchResult[];
|
|
23
|
+
/** Affected file paths grouped by operation, for the §9.1 summary. */
|
|
24
|
+
affected: {
|
|
25
|
+
added: string[];
|
|
26
|
+
modified: string[];
|
|
27
|
+
deleted: string[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Apply a full Codex `*** Begin Patch` envelope.
|
|
33
|
+
*
|
|
34
|
+
* Note: renames are reported under `modified` with the original path (spec
|
|
35
|
+
* §9.1), not as a delete + add.
|
|
36
|
+
*/
|
|
37
|
+
export async function applyCodexPatch(patchText: string, options: ApplyPatchOptions): Promise<ApplyCodexPatchResult> {
|
|
38
|
+
const hunks = parseApplyPatch(patchText);
|
|
39
|
+
|
|
40
|
+
if (hunks.length === 0) {
|
|
41
|
+
throw new ApplyPatchError("No files were modified.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const results: ApplyPatchResult[] = [];
|
|
45
|
+
const affected = {
|
|
46
|
+
added: [] as string[],
|
|
47
|
+
modified: [] as string[],
|
|
48
|
+
deleted: [] as string[],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
for (const hunk of hunks) {
|
|
52
|
+
const result = await applyPatch(hunk, options);
|
|
53
|
+
results.push(result);
|
|
54
|
+
recordAffected(affected, hunk, result);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { results, affected };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function recordAffected(
|
|
61
|
+
affected: ApplyCodexPatchResult["affected"],
|
|
62
|
+
hunk: PatchInput,
|
|
63
|
+
_result: ApplyPatchResult,
|
|
64
|
+
): void {
|
|
65
|
+
switch (hunk.op) {
|
|
66
|
+
case "create":
|
|
67
|
+
affected.added.push(hunk.path);
|
|
68
|
+
break;
|
|
69
|
+
case "delete":
|
|
70
|
+
affected.deleted.push(hunk.path);
|
|
71
|
+
break;
|
|
72
|
+
case "update":
|
|
73
|
+
affected.modified.push(hunk.path);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Format the A/M/D summary described in spec §9.1.
|
|
80
|
+
*/
|
|
81
|
+
export function formatApplyCodexPatchSummary(affected: ApplyCodexPatchResult["affected"]): string {
|
|
82
|
+
const lines = ["Success. Updated the following files:"];
|
|
83
|
+
for (const p of affected.added) lines.push(`A ${p}`);
|
|
84
|
+
for (const p of affected.modified) lines.push(`M ${p}`);
|
|
85
|
+
for (const p of affected.deleted) lines.push(`D ${p}`);
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for the Codex `apply_patch` envelope format.
|
|
3
|
+
*
|
|
4
|
+
* *** Begin Patch
|
|
5
|
+
* *** Add File: <path>
|
|
6
|
+
* +<line>
|
|
7
|
+
* *** Delete File: <path>
|
|
8
|
+
* *** Update File: <path>
|
|
9
|
+
* *** Move to: <newpath>
|
|
10
|
+
* @@ <optional context>
|
|
11
|
+
* -old
|
|
12
|
+
* +new
|
|
13
|
+
* context
|
|
14
|
+
* *** End of File
|
|
15
|
+
* *** End Patch
|
|
16
|
+
*
|
|
17
|
+
* Input is the full envelope text (optionally heredoc-wrapped). Output is a
|
|
18
|
+
* list of `PatchInput` records, each ready to hand to the existing
|
|
19
|
+
* single-file `applyPatch()` in `../modes/patch.ts`.
|
|
20
|
+
*
|
|
21
|
+
* Per spec §4.3 Lenient mode: a `<<EOF` / `<<'EOF'` / `<<"EOF"` heredoc
|
|
22
|
+
* wrapper around the whole envelope is stripped before parsing.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { ParseError } from "../diff";
|
|
26
|
+
import type { PatchInput } from "../modes/patch";
|
|
27
|
+
|
|
28
|
+
const BEGIN_PATCH_MARKER = "*** Begin Patch";
|
|
29
|
+
const END_PATCH_MARKER = "*** End Patch";
|
|
30
|
+
const ADD_FILE_MARKER = "*** Add File: ";
|
|
31
|
+
const DELETE_FILE_MARKER = "*** Delete File: ";
|
|
32
|
+
const UPDATE_FILE_MARKER = "*** Update File: ";
|
|
33
|
+
const MOVE_TO_MARKER = "*** Move to: ";
|
|
34
|
+
|
|
35
|
+
interface ParseApplyPatchOptions {
|
|
36
|
+
streaming?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse a Codex `*** Begin Patch` envelope into a list of single-file
|
|
41
|
+
* patch inputs.
|
|
42
|
+
*/
|
|
43
|
+
export function parseApplyPatch(patchText: string): PatchInput[] {
|
|
44
|
+
return parseApplyPatchWithOptions(patchText, {});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Best-effort parser for in-progress TUI previews. It tolerates missing
|
|
49
|
+
* envelope markers and incomplete trailing hunks; do not use it to apply edits.
|
|
50
|
+
*/
|
|
51
|
+
export function parseApplyPatchStreaming(patchText: string): PatchInput[] {
|
|
52
|
+
return parseApplyPatchWithOptions(patchText, { streaming: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseApplyPatchWithOptions(patchText: string, options: ParseApplyPatchOptions): PatchInput[] {
|
|
56
|
+
const streaming = options.streaming === true;
|
|
57
|
+
let lines = patchText.trim().split("\n");
|
|
58
|
+
|
|
59
|
+
// Lenient heredoc strip: <<EOF / <<'EOF' / <<"EOF" ... EOF
|
|
60
|
+
if (lines.length >= 2) {
|
|
61
|
+
const first = lines[0];
|
|
62
|
+
const last = lines[lines.length - 1].trim();
|
|
63
|
+
const validOpeners = new Set(["<<EOF", "<<'EOF'", '<<"EOF"']);
|
|
64
|
+
if (validOpeners.has(first) && last === "EOF") {
|
|
65
|
+
lines = lines.slice(1, lines.length - 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (lines.length === 0 || lines[0].trim() !== BEGIN_PATCH_MARKER) {
|
|
70
|
+
if (streaming) return [];
|
|
71
|
+
throw new ParseError("The first line of the patch must be '*** Begin Patch'");
|
|
72
|
+
}
|
|
73
|
+
const hasEndMarker = lines[lines.length - 1].trim() === END_PATCH_MARKER;
|
|
74
|
+
if (!hasEndMarker && !streaming) {
|
|
75
|
+
throw new ParseError("The last line of the patch must be '*** End Patch'");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const hunks: PatchInput[] = [];
|
|
79
|
+
let remaining = hasEndMarker ? lines.slice(1, lines.length - 1) : lines.slice(1);
|
|
80
|
+
// Line numbers are 1-based and include the `*** Begin Patch` line (= 1).
|
|
81
|
+
let lineNumber = 2;
|
|
82
|
+
|
|
83
|
+
while (remaining.length > 0) {
|
|
84
|
+
// Blank separator lines between hunks are ignored (spec §3.3).
|
|
85
|
+
if (remaining[0].trim() === "") {
|
|
86
|
+
remaining = remaining.slice(1);
|
|
87
|
+
lineNumber++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const firstLine = remaining[0].trim();
|
|
92
|
+
|
|
93
|
+
if (firstLine.startsWith(ADD_FILE_MARKER)) {
|
|
94
|
+
const path = firstLine.slice(ADD_FILE_MARKER.length);
|
|
95
|
+
let contents = "";
|
|
96
|
+
let consumed = 1;
|
|
97
|
+
|
|
98
|
+
for (let i = 1; i < remaining.length; i++) {
|
|
99
|
+
const line = remaining[i];
|
|
100
|
+
if (line.startsWith("+")) {
|
|
101
|
+
contents += `${line.slice(1)}\n`;
|
|
102
|
+
consumed++;
|
|
103
|
+
} else {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
hunks.push({ path, op: "create", diff: contents });
|
|
109
|
+
remaining = remaining.slice(consumed);
|
|
110
|
+
lineNumber += consumed;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (firstLine.startsWith(DELETE_FILE_MARKER)) {
|
|
115
|
+
const path = firstLine.slice(DELETE_FILE_MARKER.length);
|
|
116
|
+
hunks.push({ path, op: "delete" });
|
|
117
|
+
remaining = remaining.slice(1);
|
|
118
|
+
lineNumber++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (firstLine.startsWith(UPDATE_FILE_MARKER)) {
|
|
123
|
+
const path = firstLine.slice(UPDATE_FILE_MARKER.length);
|
|
124
|
+
remaining = remaining.slice(1);
|
|
125
|
+
lineNumber++;
|
|
126
|
+
|
|
127
|
+
let movePath: string | undefined;
|
|
128
|
+
if (remaining.length > 0 && remaining[0].startsWith(MOVE_TO_MARKER)) {
|
|
129
|
+
movePath = remaining[0].slice(MOVE_TO_MARKER.length);
|
|
130
|
+
remaining = remaining.slice(1);
|
|
131
|
+
lineNumber++;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The body runs until the next file-op marker or end of input.
|
|
135
|
+
// `*** End of File` is a chunk-terminator and stays inside the body —
|
|
136
|
+
// the downstream unified-diff parser handles it.
|
|
137
|
+
const diffLines: string[] = [];
|
|
138
|
+
while (remaining.length > 0) {
|
|
139
|
+
const line = remaining[0];
|
|
140
|
+
if (
|
|
141
|
+
line.startsWith("*** Add File:") ||
|
|
142
|
+
line.startsWith("*** Delete File:") ||
|
|
143
|
+
line.startsWith("*** Update File:")
|
|
144
|
+
) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
diffLines.push(line);
|
|
148
|
+
remaining = remaining.slice(1);
|
|
149
|
+
lineNumber++;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (diffLines.length === 0) {
|
|
153
|
+
if (streaming) {
|
|
154
|
+
hunks.push({ path, op: "update", rename: movePath, diff: "" });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
throw new ParseError(`Update file hunk for path '${path}' is empty`, lineNumber);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
hunks.push({ path, op: "update", rename: movePath, diff: diffLines.join("\n") });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (streaming) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
throw new ParseError(
|
|
168
|
+
`'${firstLine}' is not a valid hunk header. Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'`,
|
|
169
|
+
lineNumber,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return hunks;
|
|
174
|
+
}
|
package/src/edit/diff.ts
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* Provides diff string generation and the replace-mode edit logic
|
|
5
5
|
* used when not in patch mode.
|
|
6
6
|
*/
|
|
7
|
-
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
8
7
|
import * as Diff from "diff";
|
|
9
8
|
import { resolveToCwd } from "../tools/path-utils";
|
|
10
9
|
import { DEFAULT_FUZZY_THRESHOLD, EditMatchError, findMatch } from "./modes/replace";
|
|
11
10
|
import { adjustIndentation, normalizeToLF, stripBom } from "./normalize";
|
|
11
|
+
import { readEditFileText } from "./read-file";
|
|
12
12
|
|
|
13
13
|
export interface DiffResult {
|
|
14
14
|
diff: string;
|
|
@@ -275,17 +275,6 @@ function formatOccurrenceMatchError(
|
|
|
275
275
|
return `Found ${occurrences} occurrences${pathSuffix}${moreMsg}:\n\n${previews}\n\nAdd more context lines to disambiguate.`;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
async function readFileTextForDiff(path: string, absolutePath: string): Promise<string> {
|
|
279
|
-
try {
|
|
280
|
-
return await Bun.file(absolutePath).text();
|
|
281
|
-
} catch (error) {
|
|
282
|
-
if (isEnoent(error)) {
|
|
283
|
-
throw new Error(`File not found: ${path}`);
|
|
284
|
-
}
|
|
285
|
-
throw error;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
278
|
export function normalizeDiff(diff: string): string {
|
|
290
279
|
let lines = diff.split("\n");
|
|
291
280
|
|
|
@@ -761,12 +750,12 @@ export async function computeEditDiff(
|
|
|
761
750
|
if (oldText.length === 0) {
|
|
762
751
|
return { error: "oldText must not be empty." };
|
|
763
752
|
}
|
|
764
|
-
const absolutePath = resolveToCwd(path, cwd);
|
|
765
753
|
|
|
766
754
|
try {
|
|
755
|
+
const absolutePath = resolveToCwd(path, cwd);
|
|
767
756
|
let rawContent: string;
|
|
768
757
|
try {
|
|
769
|
-
rawContent = await
|
|
758
|
+
rawContent = await readEditFileText(absolutePath, path);
|
|
770
759
|
} catch (error) {
|
|
771
760
|
const message = error instanceof Error ? error.message : String(error);
|
|
772
761
|
return { error: message || `Unable to read ${path}` };
|