zero-ai-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MERGE_REPORT.md +265 -0
- package/README.md +127 -0
- package/package.json +55 -0
- package/packages/cli/src/index.ts +271 -0
- package/packages/cli/src/interactive.ts +190 -0
- package/packages/cli/src/once.ts +50 -0
- package/packages/core/src/agent/config-loader.ts +174 -0
- package/packages/core/src/agent/engine.ts +266 -0
- package/packages/core/src/agent/registry-tools.ts +58 -0
- package/packages/core/src/agent/registry.ts +213 -0
- package/packages/core/src/commands/index.ts +116 -0
- package/packages/core/src/config/index.ts +139 -0
- package/packages/core/src/confirmation/message-bus.ts +206 -0
- package/packages/core/src/diff/index.ts +232 -0
- package/packages/core/src/diff-detect/index.ts +207 -0
- package/packages/core/src/edit-coder/index.ts +194 -0
- package/packages/core/src/events/index.ts +151 -0
- package/packages/core/src/file-tracker/index.ts +183 -0
- package/packages/core/src/git/index.ts +305 -0
- package/packages/core/src/history/index.ts +236 -0
- package/packages/core/src/image/index.ts +131 -0
- package/packages/core/src/index.ts +131 -0
- package/packages/core/src/lsp/index.ts +251 -0
- package/packages/core/src/mcp/index.ts +186 -0
- package/packages/core/src/memory/index.ts +141 -0
- package/packages/core/src/memory/prompts.ts +52 -0
- package/packages/core/src/orchestrator/protocol.ts +140 -0
- package/packages/core/src/orchestrator/server.ts +319 -0
- package/packages/core/src/output/index.ts +164 -0
- package/packages/core/src/permissions/index.ts +126 -0
- package/packages/core/src/prompts/engine.ts +188 -0
- package/packages/core/src/providers/registry.ts +687 -0
- package/packages/core/src/routing/model-router.ts +160 -0
- package/packages/core/src/server/index.ts +232 -0
- package/packages/core/src/session/index.ts +174 -0
- package/packages/core/src/session/service.ts +322 -0
- package/packages/core/src/skills/index.ts +205 -0
- package/packages/core/src/streaming/index.ts +166 -0
- package/packages/core/src/sub-agent/index.ts +179 -0
- package/packages/core/src/tools/builtin.ts +568 -0
- package/packages/core/src/types.ts +356 -0
- package/packages/sdk/src/index.ts +247 -0
- package/packages/tui/src/index.ts +141 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO Configuration System
|
|
3
|
+
* Handles loading, validating, and managing ZERO configuration
|
|
4
|
+
*
|
|
5
|
+
* Sources:
|
|
6
|
+
* - gemini-cli: config with extensions
|
|
7
|
+
* - crush: JSON config with schema
|
|
8
|
+
* - opencode: configuration management
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs/promises";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import type { ZeroConfig } from "../types.js";
|
|
14
|
+
|
|
15
|
+
const CONFIG_FILE_NAME = "zero.config.json";
|
|
16
|
+
const ZERO_DIR = ".zero";
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_CONFIG: ZeroConfig = {
|
|
19
|
+
version: 1,
|
|
20
|
+
defaultProvider: "openai",
|
|
21
|
+
defaultModel: "gpt-4o",
|
|
22
|
+
providers: {
|
|
23
|
+
openai: {
|
|
24
|
+
type: "openai",
|
|
25
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
26
|
+
},
|
|
27
|
+
anthropic: {
|
|
28
|
+
type: "anthropic",
|
|
29
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
30
|
+
},
|
|
31
|
+
google: {
|
|
32
|
+
type: "google",
|
|
33
|
+
apiKey: process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY,
|
|
34
|
+
},
|
|
35
|
+
ollama: {
|
|
36
|
+
type: "ollama",
|
|
37
|
+
baseUrl: "http://localhost:11434/v1",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
permissions: {
|
|
41
|
+
fileRead: "allow",
|
|
42
|
+
fileWrite: "ask",
|
|
43
|
+
shellExecution: "ask",
|
|
44
|
+
networkAccess: "ask",
|
|
45
|
+
mcpAccess: "allow",
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export class ConfigManager {
|
|
50
|
+
private config: ZeroConfig;
|
|
51
|
+
private configPath: string;
|
|
52
|
+
private projectPath: string;
|
|
53
|
+
|
|
54
|
+
constructor(projectPath: string) {
|
|
55
|
+
this.projectPath = projectPath;
|
|
56
|
+
this.configPath = path.join(projectPath, CONFIG_FILE_NAME);
|
|
57
|
+
this.config = { ...DEFAULT_CONFIG };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load configuration from file
|
|
62
|
+
*/
|
|
63
|
+
async load(): Promise<ZeroConfig> {
|
|
64
|
+
try {
|
|
65
|
+
const data = await fs.readFile(this.configPath, "utf-8");
|
|
66
|
+
const loaded = JSON.parse(data);
|
|
67
|
+
this.config = { ...DEFAULT_CONFIG, ...loaded };
|
|
68
|
+
} catch {
|
|
69
|
+
// Use defaults if config file doesn't exist
|
|
70
|
+
this.config = { ...DEFAULT_CONFIG };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Also check for .zero/ directory config
|
|
74
|
+
try {
|
|
75
|
+
const zeroDirConfig = path.join(this.projectPath, ZERO_DIR, "config.json");
|
|
76
|
+
const data = await fs.readFile(zeroDirConfig, "utf-8");
|
|
77
|
+
const loaded = JSON.parse(data);
|
|
78
|
+
this.config = { ...this.config, ...loaded };
|
|
79
|
+
} catch {}
|
|
80
|
+
|
|
81
|
+
return this.config;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Save configuration to file
|
|
86
|
+
*/
|
|
87
|
+
async save(): Promise<void> {
|
|
88
|
+
await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get current configuration
|
|
93
|
+
*/
|
|
94
|
+
get(): ZeroConfig {
|
|
95
|
+
return { ...this.config };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Update configuration
|
|
100
|
+
*/
|
|
101
|
+
update(partial: Partial<ZeroConfig>): ZeroConfig {
|
|
102
|
+
this.config = { ...this.config, ...partial };
|
|
103
|
+
return this.config;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the ZERO data directory path
|
|
108
|
+
*/
|
|
109
|
+
getDataDir(): string {
|
|
110
|
+
return path.join(this.projectPath, ZERO_DIR);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Initialize ZERO project structure
|
|
115
|
+
*/
|
|
116
|
+
async init(): Promise<void> {
|
|
117
|
+
const zeroDir = this.getDataDir();
|
|
118
|
+
await fs.mkdir(zeroDir, { recursive: true });
|
|
119
|
+
await fs.mkdir(path.join(zeroDir, "sessions"), { recursive: true });
|
|
120
|
+
await fs.mkdir(path.join(zeroDir, "memory"), { recursive: true });
|
|
121
|
+
await fs.mkdir(path.join(zeroDir, "logs"), { recursive: true });
|
|
122
|
+
|
|
123
|
+
// Create default config
|
|
124
|
+
await this.save();
|
|
125
|
+
|
|
126
|
+
// Create .gitignore for .zero directory
|
|
127
|
+
const gitignore = path.join(zeroDir, ".gitignore");
|
|
128
|
+
try {
|
|
129
|
+
await fs.writeFile(gitignore, "sessions/\nmemory/\nlogs/\n*.log\n");
|
|
130
|
+
} catch {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolve environment variables in config
|
|
135
|
+
*/
|
|
136
|
+
resolveEnv(value: string): string {
|
|
137
|
+
return value.replace(/\$\{(\w+)\}/g, (_, key) => process.env[key] || "");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO Message Bus
|
|
3
|
+
* Adapted from: qwen-code (Alibaba) - MIT
|
|
4
|
+
*
|
|
5
|
+
* Event-driven message bus for tool confirmations and hook execution.
|
|
6
|
+
* Supports request-response patterns with correlation IDs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { EventEmitter } from "node:events";
|
|
11
|
+
|
|
12
|
+
export enum MessageBusType {
|
|
13
|
+
TOOL_CONFIRMATION_REQUEST = "tool-confirmation-request",
|
|
14
|
+
TOOL_CONFIRMATION_RESPONSE = "tool-confirmation-response",
|
|
15
|
+
TOOL_EXECUTION_SUCCESS = "tool-execution-success",
|
|
16
|
+
TOOL_EXECUTION_FAILURE = "tool-execution-failure",
|
|
17
|
+
HOOK_EXECUTION_REQUEST = "hook-execution-request",
|
|
18
|
+
HOOK_EXECUTION_RESPONSE = "hook-execution-response",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ToolConfirmationRequest {
|
|
22
|
+
type: MessageBusType.TOOL_CONFIRMATION_REQUEST;
|
|
23
|
+
toolName: string;
|
|
24
|
+
toolArgs: Record<string, unknown>;
|
|
25
|
+
correlationId: string;
|
|
26
|
+
details?: SerializableConfirmationDetails;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ToolConfirmationResponse {
|
|
30
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE;
|
|
31
|
+
correlationId: string;
|
|
32
|
+
confirmed: boolean;
|
|
33
|
+
payload?: unknown;
|
|
34
|
+
requiresUserConfirmation?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SerializableConfirmationDetails =
|
|
38
|
+
| {
|
|
39
|
+
type: "info";
|
|
40
|
+
title: string;
|
|
41
|
+
prompt: string;
|
|
42
|
+
urls?: string[];
|
|
43
|
+
}
|
|
44
|
+
| {
|
|
45
|
+
type: "edit";
|
|
46
|
+
title: string;
|
|
47
|
+
fileName: string;
|
|
48
|
+
filePath: string;
|
|
49
|
+
fileDiff: string;
|
|
50
|
+
originalContent: string | null;
|
|
51
|
+
newContent: string;
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
type: "exec";
|
|
55
|
+
title: string;
|
|
56
|
+
command: string;
|
|
57
|
+
rootCommand: string;
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
type: "mcp";
|
|
61
|
+
title: string;
|
|
62
|
+
serverName: string;
|
|
63
|
+
toolName: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface ToolExecutionSuccess<T = unknown> {
|
|
67
|
+
type: MessageBusType.TOOL_EXECUTION_SUCCESS;
|
|
68
|
+
toolName: string;
|
|
69
|
+
result: T;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ToolExecutionFailure {
|
|
73
|
+
type: MessageBusType.TOOL_EXECUTION_FAILURE;
|
|
74
|
+
toolName: string;
|
|
75
|
+
error: Error;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface HookExecutionRequest {
|
|
79
|
+
type: MessageBusType.HOOK_EXECUTION_REQUEST;
|
|
80
|
+
eventName: string;
|
|
81
|
+
input: Record<string, unknown>;
|
|
82
|
+
correlationId: string;
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface HookExecutionResponse {
|
|
87
|
+
type: MessageBusType.HOOK_EXECUTION_RESPONSE;
|
|
88
|
+
correlationId: string;
|
|
89
|
+
success: boolean;
|
|
90
|
+
output?: Record<string, unknown>;
|
|
91
|
+
error?: Error;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type Message =
|
|
95
|
+
| ToolConfirmationRequest
|
|
96
|
+
| ToolConfirmationResponse
|
|
97
|
+
| ToolExecutionSuccess
|
|
98
|
+
| ToolExecutionFailure
|
|
99
|
+
| HookExecutionRequest
|
|
100
|
+
| HookExecutionResponse;
|
|
101
|
+
|
|
102
|
+
export class MessageBus extends EventEmitter {
|
|
103
|
+
private isValidMessage(message: Message): boolean {
|
|
104
|
+
if (!message || !message.type) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
message.type === MessageBusType.TOOL_CONFIRMATION_REQUEST &&
|
|
110
|
+
!("correlationId" in message)
|
|
111
|
+
) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async publish(message: Message): Promise<void> {
|
|
119
|
+
try {
|
|
120
|
+
if (!this.isValidMessage(message)) {
|
|
121
|
+
throw new Error(`Invalid message structure: ${JSON.stringify(message)}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (message.type === MessageBusType.TOOL_CONFIRMATION_REQUEST) {
|
|
125
|
+
// Auto-confirm by default
|
|
126
|
+
this.emit(MessageBusType.TOOL_CONFIRMATION_RESPONSE, {
|
|
127
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
128
|
+
correlationId: message.correlationId,
|
|
129
|
+
confirmed: true,
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
this.emit(message.type, message);
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
this.emit("error", error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
subscribe<T extends Message>(
|
|
140
|
+
type: T["type"],
|
|
141
|
+
listener: (message: T) => void
|
|
142
|
+
): void {
|
|
143
|
+
this.on(type, listener);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
unsubscribe<T extends Message>(
|
|
147
|
+
type: T["type"],
|
|
148
|
+
listener: (message: T) => void
|
|
149
|
+
): void {
|
|
150
|
+
this.off(type, listener);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Request-response pattern: Publish a message and wait for a correlated response
|
|
155
|
+
*/
|
|
156
|
+
async request<TRequest extends Message, TResponse extends Message>(
|
|
157
|
+
request: Omit<TRequest, "correlationId">,
|
|
158
|
+
responseType: TResponse["type"],
|
|
159
|
+
timeoutMs: number = 60000,
|
|
160
|
+
signal?: AbortSignal
|
|
161
|
+
): Promise<TResponse> {
|
|
162
|
+
const correlationId = randomUUID();
|
|
163
|
+
|
|
164
|
+
return new Promise<TResponse>((resolve, reject) => {
|
|
165
|
+
if (signal?.aborted) {
|
|
166
|
+
reject(new Error("Request aborted"));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const timeoutId = setTimeout(() => {
|
|
171
|
+
cleanup();
|
|
172
|
+
reject(new Error(`Request timed out waiting for ${responseType}`));
|
|
173
|
+
}, timeoutMs);
|
|
174
|
+
|
|
175
|
+
const cleanup = () => {
|
|
176
|
+
clearTimeout(timeoutId);
|
|
177
|
+
this.unsubscribe(responseType, responseHandler);
|
|
178
|
+
if (signal) {
|
|
179
|
+
signal.removeEventListener("abort", abortHandler);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const abortHandler = () => {
|
|
184
|
+
cleanup();
|
|
185
|
+
reject(new Error("Request aborted"));
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (signal) {
|
|
189
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const responseHandler = (response: TResponse) => {
|
|
193
|
+
if (
|
|
194
|
+
"correlationId" in response &&
|
|
195
|
+
response.correlationId === correlationId
|
|
196
|
+
) {
|
|
197
|
+
cleanup();
|
|
198
|
+
resolve(response);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
this.subscribe<TResponse>(responseType, responseHandler);
|
|
203
|
+
this.publish({ ...request, correlationId } as TRequest);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO Diff System
|
|
3
|
+
* Smart file editing with diff generation and application
|
|
4
|
+
*
|
|
5
|
+
* Sources:
|
|
6
|
+
* - aider: search/replace diff editing, unified diff
|
|
7
|
+
* - cline: block-based editing
|
|
8
|
+
* - kilocode: precise file modification
|
|
9
|
+
* - crush: diff detection and application
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface DiffBlock {
|
|
13
|
+
type: "context" | "add" | "remove";
|
|
14
|
+
lineNumber: number;
|
|
15
|
+
content: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FileDiff {
|
|
19
|
+
filePath: string;
|
|
20
|
+
oldContent: string;
|
|
21
|
+
newContent: string;
|
|
22
|
+
blocks: DiffBlock[];
|
|
23
|
+
stats: { added: number; removed: number; unchanged: number };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate a unified diff between two strings
|
|
28
|
+
*/
|
|
29
|
+
export function generateDiff(oldContent: string, newContent: string, filePath = "file"): FileDiff {
|
|
30
|
+
const oldLines = oldContent.split("\n");
|
|
31
|
+
const newLines = newContent.split("\n");
|
|
32
|
+
const blocks: DiffBlock[] = [];
|
|
33
|
+
|
|
34
|
+
let added = 0;
|
|
35
|
+
let removed = 0;
|
|
36
|
+
let unchanged = 0;
|
|
37
|
+
|
|
38
|
+
// Simple LCS-based diff
|
|
39
|
+
const lcs = computeLCS(oldLines, newLines);
|
|
40
|
+
let i = 0;
|
|
41
|
+
let j = 0;
|
|
42
|
+
let k = 0;
|
|
43
|
+
|
|
44
|
+
while (i < oldLines.length || j < newLines.length) {
|
|
45
|
+
if (k < lcs.length && i < oldLines.length && oldLines[i] === lcs[k] && j < newLines.length && newLines[j] === lcs[k]) {
|
|
46
|
+
blocks.push({ type: "context", lineNumber: i + 1, content: oldLines[i] });
|
|
47
|
+
unchanged++;
|
|
48
|
+
i++;
|
|
49
|
+
j++;
|
|
50
|
+
k++;
|
|
51
|
+
} else if (j < newLines.length && (k >= lcs.length || newLines[j] !== lcs[k])) {
|
|
52
|
+
blocks.push({ type: "add", lineNumber: j + 1, content: newLines[j] });
|
|
53
|
+
added++;
|
|
54
|
+
j++;
|
|
55
|
+
} else if (i < oldLines.length && (k >= lcs.length || oldLines[i] !== lcs[k])) {
|
|
56
|
+
blocks.push({ type: "remove", lineNumber: i + 1, content: oldLines[i] });
|
|
57
|
+
removed++;
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
filePath,
|
|
64
|
+
oldContent,
|
|
65
|
+
newContent,
|
|
66
|
+
blocks,
|
|
67
|
+
stats: { added, removed, unchanged },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format diff as unified diff string
|
|
73
|
+
*/
|
|
74
|
+
export function formatUnifiedDiff(diff: FileDiff): string {
|
|
75
|
+
const lines: string[] = [];
|
|
76
|
+
lines.push(`--- a/${diff.filePath}`);
|
|
77
|
+
lines.push(`+++ b/${diff.filePath}`);
|
|
78
|
+
lines.push(`@@ -1,${diff.oldContent.split("\n").length} +1,${diff.newContent.split("\n").length} @@`);
|
|
79
|
+
|
|
80
|
+
for (const block of diff.blocks) {
|
|
81
|
+
switch (block.type) {
|
|
82
|
+
case "context":
|
|
83
|
+
lines.push(` ${block.content}`);
|
|
84
|
+
break;
|
|
85
|
+
case "add":
|
|
86
|
+
lines.push(`+${block.content}`);
|
|
87
|
+
break;
|
|
88
|
+
case "remove":
|
|
89
|
+
lines.push(`-${block.content}`);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Apply a search/replace edit (aider-style)
|
|
99
|
+
*/
|
|
100
|
+
export function applySearchReplace(
|
|
101
|
+
content: string,
|
|
102
|
+
search: string,
|
|
103
|
+
replace: string,
|
|
104
|
+
): { success: boolean; result: string; error?: string } {
|
|
105
|
+
const count = content.split(search).length - 1;
|
|
106
|
+
|
|
107
|
+
if (count === 0) {
|
|
108
|
+
return { success: false, result: content, error: "Search text not found in file" };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (count > 1) {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
result: content,
|
|
115
|
+
error: `Search text found ${count} times. Provide more context for unique match.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
result: content.replace(search, replace),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Apply multiple search/replace blocks (aider-style multi-edit)
|
|
127
|
+
*/
|
|
128
|
+
export function applyMultiEdit(
|
|
129
|
+
content: string,
|
|
130
|
+
edits: Array<{ search: string; replace: string }>,
|
|
131
|
+
): { success: boolean; result: string; errors: string[] } {
|
|
132
|
+
let result = content;
|
|
133
|
+
const errors: string[] = [];
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < edits.length; i++) {
|
|
136
|
+
const { search, replace } = edits[i];
|
|
137
|
+
const applied = applySearchReplace(result, search, replace);
|
|
138
|
+
|
|
139
|
+
if (!applied.success) {
|
|
140
|
+
errors.push(`Edit ${i + 1}: ${applied.error}`);
|
|
141
|
+
} else {
|
|
142
|
+
result = applied.result;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { success: errors.length === 0, result, errors };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Smart merge: apply changes while preserving untouched sections
|
|
151
|
+
*/
|
|
152
|
+
export function smartMerge(
|
|
153
|
+
original: string,
|
|
154
|
+
modified: string,
|
|
155
|
+
base: string,
|
|
156
|
+
): { success: boolean; result: string; conflicts: string[] } {
|
|
157
|
+
// Simple three-way merge
|
|
158
|
+
const origLines = original.split("\n");
|
|
159
|
+
const modLines = modified.split("\n");
|
|
160
|
+
const baseLines = base.split("\n");
|
|
161
|
+
|
|
162
|
+
const result: string[] = [];
|
|
163
|
+
const conflicts: string[] = [];
|
|
164
|
+
|
|
165
|
+
const maxLen = Math.max(origLines.length, modLines.length, baseLines.length);
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < maxLen; i++) {
|
|
168
|
+
const origLine = origLines[i] ?? "";
|
|
169
|
+
const modLine = modLines[i] ?? "";
|
|
170
|
+
const baseLine = baseLines[i] ?? "";
|
|
171
|
+
|
|
172
|
+
if (modLine === baseLine) {
|
|
173
|
+
// No change from base -> use original
|
|
174
|
+
result.push(origLine);
|
|
175
|
+
} else if (origLine === baseLine) {
|
|
176
|
+
// Change from base in modified -> use modified
|
|
177
|
+
result.push(modLine);
|
|
178
|
+
} else if (origLine === modLine) {
|
|
179
|
+
// Same change in both -> use either
|
|
180
|
+
result.push(origLine);
|
|
181
|
+
} else {
|
|
182
|
+
// Conflict
|
|
183
|
+
conflicts.push(`Line ${i + 1}: original="${origLine}" vs modified="${modLine}"`);
|
|
184
|
+
result.push(`<<<<<<< ORIGINAL`);
|
|
185
|
+
result.push(origLine);
|
|
186
|
+
result.push(`=======`);
|
|
187
|
+
result.push(modLine);
|
|
188
|
+
result.push(`>>>>>>> MODIFIED`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
success: conflicts.length === 0,
|
|
194
|
+
result: result.join("\n"),
|
|
195
|
+
conflicts,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Helper: Longest Common Subsequence
|
|
200
|
+
function computeLCS(a: string[], b: string[]): string[] {
|
|
201
|
+
const m = a.length;
|
|
202
|
+
const n = b.length;
|
|
203
|
+
const dp: number[][] = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
|
204
|
+
|
|
205
|
+
for (let i = 1; i <= m; i++) {
|
|
206
|
+
for (let j = 1; j <= n; j++) {
|
|
207
|
+
if (a[i - 1] === b[j - 1]) {
|
|
208
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
209
|
+
} else {
|
|
210
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Backtrack to find LCS
|
|
216
|
+
const lcs: string[] = [];
|
|
217
|
+
let i = m;
|
|
218
|
+
let j = n;
|
|
219
|
+
while (i > 0 && j > 0) {
|
|
220
|
+
if (a[i - 1] === b[j - 1]) {
|
|
221
|
+
lcs.unshift(a[i - 1]);
|
|
222
|
+
i--;
|
|
223
|
+
j--;
|
|
224
|
+
} else if (dp[i - 1][j] > dp[i][j - 1]) {
|
|
225
|
+
i--;
|
|
226
|
+
} else {
|
|
227
|
+
j--;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return lcs;
|
|
232
|
+
}
|