Skip to content

Commit 330608f

Browse files
committed
feat: align backend to CLI v0.1.24
1 parent bc5bbbe commit 330608f

39 files changed

Lines changed: 4519 additions & 1623 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"resources/**",
2323
"templates/tools/**",
2424
"templates/prompts/**",
25+
"templates/skills/**",
2526
"README.md",
2627
"README_cn.md",
2728
"README_en.md",

src/common/bash-timeout.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const DEFAULT_BASH_TIMEOUT_MS = 10 * 60 * 1000;
2+
export const MIN_BASH_TIMEOUT_MS = 60 * 1000;
3+
export const BASH_TIMEOUT_INCREMENT_MS = 5 * 60 * 1000;
4+
export const BASH_TIMEOUT_DECREMENT_MS = 60 * 1000;
5+
6+
export function clampBashTimeoutMs(timeoutMs: number, minTimeoutMs: number = MIN_BASH_TIMEOUT_MS): number {
7+
if (!Number.isFinite(timeoutMs)) {
8+
return DEFAULT_BASH_TIMEOUT_MS;
9+
}
10+
const minimum = Number.isFinite(minTimeoutMs) ? Math.max(1, Math.round(minTimeoutMs)) : MIN_BASH_TIMEOUT_MS;
11+
return Math.max(minimum, Math.round(timeoutMs));
12+
}

src/common/debug-logger.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,17 @@ export function getDebugLogPath(): string {
3737
return path.join(os.homedir(), ".deepcode", "logs", DEBUG_LOG_FILE);
3838
}
3939

40-
export function normalizeDebugError(error: unknown): {
41-
name: string;
42-
message: string;
43-
stack?: string;
44-
} {
40+
export function normalizeDebugError(error: unknown): { name: string; message: string; stack?: string } {
4541
if (error instanceof Error) {
4642
return {
4743
name: error.name,
4844
message: error.message,
49-
stack: error.stack
45+
stack: error.stack,
5046
};
5147
}
5248
return {
5349
name: "UnknownError",
54-
message: String(error)
50+
message: String(error),
5551
};
5652
}
5753

src/common/error-logger.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,13 @@ export function logApiError(entry: ApiErrorLogEntry): void {
104104
error: {
105105
name: entry.error.name,
106106
message: maskSensitive(entry.error.message),
107-
stack: entry.error.stack ? maskSensitive(entry.error.stack) : undefined
107+
stack: entry.error.stack ? maskSensitive(entry.error.stack) : undefined,
108108
},
109-
request: sanitizeRequestPayload(entry.request)
109+
request: sanitizeRequestPayload(entry.request),
110110
};
111111

112112
if (entry.response !== undefined) {
113-
logLine.response =
114-
typeof entry.response === "string" ? maskSensitive(entry.response) : entry.response;
113+
logLine.response = typeof entry.response === "string" ? maskSensitive(entry.response) : entry.response;
115114
}
116115

117116
const newLine = JSON.stringify(logLine) + "\n";

src/common/file-history.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import * as childProcess from "child_process";
2+
import * as fs from "fs";
3+
import * as path from "path";
4+
5+
const FILE_HISTORY_AUTHOR_NAME = "DeepCode Checkpoint";
6+
const FILE_HISTORY_AUTHOR_EMAIL = "deepcode-checkpoint@localhost";
7+
8+
export class GitFileHistory {
9+
constructor(
10+
private readonly projectRoot: string,
11+
private readonly gitDir: string
12+
) {}
13+
14+
ensureSession(sessionId: string): string | undefined {
15+
const branchRef = this.getSessionBranchRef(sessionId);
16+
if (!branchRef) {
17+
return undefined;
18+
}
19+
20+
try {
21+
if (!fs.existsSync(this.gitDir)) {
22+
fs.mkdirSync(path.dirname(this.gitDir), { recursive: true });
23+
this.runGit(["init"], { includeWorkTree: true });
24+
}
25+
26+
const current = this.getCurrentCheckpointHash(sessionId);
27+
if (current) {
28+
return current;
29+
}
30+
31+
const emptyTree = this.runGit(["mktree"], { includeWorkTree: false, input: "" }).trim();
32+
const commitHash = this.createCommit(emptyTree, null, "Initial checkpoint");
33+
this.runGit(["update-ref", branchRef, commitHash], { includeWorkTree: false });
34+
return commitHash;
35+
} catch {
36+
return undefined;
37+
}
38+
}
39+
40+
getCurrentCheckpointHash(sessionId: string): string | undefined {
41+
const branchRef = this.getSessionBranchRef(sessionId);
42+
if (!branchRef || !fs.existsSync(this.gitDir)) {
43+
return undefined;
44+
}
45+
46+
try {
47+
const hash = this.runGit(["rev-parse", "--verify", `${branchRef}^{commit}`], {
48+
includeWorkTree: false,
49+
}).trim();
50+
return isCommitHash(hash) ? hash : undefined;
51+
} catch {
52+
return undefined;
53+
}
54+
}
55+
56+
recordCheckpoint(sessionId: string, filePaths: string[], message: string): string | undefined {
57+
const branchRef = this.getSessionBranchRef(sessionId);
58+
if (!branchRef) {
59+
return undefined;
60+
}
61+
62+
const relativePaths = filePaths
63+
.map((filePath) => this.toProjectRelativeGitPath(filePath))
64+
.filter((filePath): filePath is string => Boolean(filePath));
65+
if (relativePaths.length === 0) {
66+
return this.getCurrentCheckpointHash(sessionId);
67+
}
68+
69+
try {
70+
const parentHash = this.ensureSession(sessionId);
71+
if (!parentHash) {
72+
return undefined;
73+
}
74+
this.runGit(["read-tree", "--reset", branchRef], { includeWorkTree: true });
75+
this.runGit(["add", "-f", "-A", "--", ...relativePaths], { includeWorkTree: true });
76+
const treeHash = this.runGit(["write-tree"], { includeWorkTree: false }).trim();
77+
const parentTreeHash = this.runGit(["rev-parse", `${parentHash}^{tree}`], {
78+
includeWorkTree: false,
79+
}).trim();
80+
if (treeHash === parentTreeHash) {
81+
return parentHash;
82+
}
83+
84+
const commitHash = this.createCommit(treeHash, parentHash, message);
85+
this.runGit(["update-ref", branchRef, commitHash, parentHash], { includeWorkTree: false });
86+
return commitHash;
87+
} catch {
88+
return undefined;
89+
}
90+
}
91+
92+
canRestore(sessionId: string, checkpointHash: string): boolean {
93+
if (!isCommitHash(checkpointHash)) {
94+
return false;
95+
}
96+
if (!this.getSessionBranchRef(sessionId)) {
97+
return false;
98+
}
99+
if (!fs.existsSync(this.gitDir)) {
100+
return false;
101+
}
102+
103+
try {
104+
this.runGit(["cat-file", "-e", `${checkpointHash}^{commit}`], { includeWorkTree: false });
105+
return true;
106+
} catch {
107+
return false;
108+
}
109+
}
110+
111+
restore(sessionId: string, checkpointHash: string): void {
112+
if (!isCommitHash(checkpointHash)) {
113+
throw new Error("Invalid checkpoint hash.");
114+
}
115+
const branchRef = this.getSessionBranchRef(sessionId);
116+
if (!branchRef || !fs.existsSync(this.gitDir)) {
117+
throw new Error("File history Git repository was not found for this project.");
118+
}
119+
this.runGit(["cat-file", "-e", `${checkpointHash}^{commit}`], { includeWorkTree: false });
120+
121+
try {
122+
this.runGit(["read-tree", "--reset", branchRef], { includeWorkTree: true });
123+
} catch {
124+
// If the session branch is missing, fall back to the target tree only.
125+
// The target checkpoint has already been validated above.
126+
}
127+
this.runGit(["read-tree", "--reset", "-u", checkpointHash], { includeWorkTree: true });
128+
this.runGit(["update-ref", branchRef, checkpointHash], { includeWorkTree: false });
129+
}
130+
131+
private getSessionBranchRef(sessionId: string): string | null {
132+
if (!/^[A-Za-z0-9._-]+$/.test(sessionId)) {
133+
return null;
134+
}
135+
return `refs/heads/${sessionId}`;
136+
}
137+
138+
private createCommit(treeHash: string, parentHash: string | null, message: string): string {
139+
const args = ["commit-tree", treeHash];
140+
if (parentHash) {
141+
args.push("-p", parentHash);
142+
}
143+
args.push("-m", message);
144+
return this.runGit(args, {
145+
includeWorkTree: false,
146+
env: getFileHistoryGitEnv(),
147+
}).trim();
148+
}
149+
150+
private toProjectRelativeGitPath(filePath: string): string | null {
151+
const absolutePath = path.resolve(filePath);
152+
const relativePath = path.relative(this.projectRoot, absolutePath);
153+
if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
154+
return null;
155+
}
156+
return relativePath.split(path.sep).join("/");
157+
}
158+
159+
private runGit(
160+
args: string[],
161+
options: { includeWorkTree: boolean; input?: string; env?: NodeJS.ProcessEnv }
162+
): string {
163+
const gitArgs = ["-c", "core.autocrlf=false", "-c", "core.eol=lf", `--git-dir=${this.gitDir}`];
164+
if (options.includeWorkTree) {
165+
gitArgs.push(`--work-tree=${this.projectRoot}`);
166+
}
167+
gitArgs.push(...args);
168+
const result = childProcess.spawnSync("git", gitArgs, {
169+
encoding: "utf8",
170+
input: options.input,
171+
env: options.env,
172+
stdio: ["pipe", "pipe", "pipe"],
173+
});
174+
if (result.status !== 0) {
175+
const detail = (result.stderr || result.stdout || "").trim();
176+
throw new Error(detail || `git ${args.join(" ")} failed`);
177+
}
178+
return result.stdout ?? "";
179+
}
180+
}
181+
182+
function getFileHistoryGitEnv(): NodeJS.ProcessEnv {
183+
return {
184+
...process.env,
185+
GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || FILE_HISTORY_AUTHOR_NAME,
186+
GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL || FILE_HISTORY_AUTHOR_EMAIL,
187+
GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || FILE_HISTORY_AUTHOR_NAME,
188+
GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || FILE_HISTORY_AUTHOR_EMAIL,
189+
};
190+
}
191+
192+
function isCommitHash(value: string): boolean {
193+
return /^[0-9a-f]{40}$/i.test(value);
194+
}

src/common/file-utils.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function readTextFileWithMetadata(filePath: string): FileReadMetadata {
3535
content: normalizeContent(raw),
3636
encoding,
3737
lineEndings: detectLineEndings(raw),
38-
timestamp: Math.floor(stat.mtimeMs)
38+
timestamp: Math.floor(stat.mtimeMs),
3939
};
4040
}
4141

@@ -61,10 +61,7 @@ export function hasFileChangedSinceState(filePath: string, state: FileState): bo
6161
return false;
6262
}
6363

64-
const isFullRead =
65-
!state.isPartialView &&
66-
typeof state.offset === "undefined" &&
67-
typeof state.limit === "undefined";
64+
const isFullRead = !state.isPartialView && typeof state.offset === "undefined" && typeof state.limit === "undefined";
6865

6966
return !(isFullRead && current.content === state.content);
7067
}
@@ -86,11 +83,7 @@ export function buildDiffPreview(
8683
const newLines = toDiffLines(updated);
8784

8885
let prefix = 0;
89-
while (
90-
prefix < oldLines.length &&
91-
prefix < newLines.length &&
92-
oldLines[prefix] === newLines[prefix]
93-
) {
86+
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) {
9487
prefix += 1;
9588
}
9689

@@ -111,7 +104,7 @@ export function buildDiffPreview(
111104
const previewLines = [
112105
`--- ${original === null ? "/dev/null" : `a/${filePath}`}`,
113106
`+++ b/${filePath}`,
114-
`@@ -${oldStart},${oldChanged.length} +${newStart},${newChanged.length} @@`
107+
`@@ -${oldStart},${oldChanged.length} +${newStart},${newChanged.length} @@`,
115108
];
116109

117110
if (prefix > 0) {

src/common/model-capabilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export const NON_MULTIMODAL_MODELS = new Set([
44
"deepseek-v4-pro",
55
"deepseek-v4-flash",
66
"deepseek-chat",
7-
"deepseek-reasoner"
7+
"deepseek-reasoner",
88
]);
99

1010
export function defaultsToThinkingMode(model: string): boolean {

src/common/notify.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,49 @@ export function formatDurationSeconds(durationMs: number): string {
1616
return String(Math.floor(safeMs / 1000));
1717
}
1818

19+
export type NotifyContext = {
20+
status?: string;
21+
failReason?: string;
22+
body?: string;
23+
title?: string;
24+
};
25+
1926
export function buildNotifyEnv(
2027
durationMs: number,
21-
baseEnv: NodeJS.ProcessEnv = process.env
28+
baseEnv: NodeJS.ProcessEnv = process.env,
29+
context: NotifyContext = {}
2230
): NodeJS.ProcessEnv {
23-
return {
31+
const env: NodeJS.ProcessEnv = {
2432
...baseEnv,
25-
DURATION: formatDurationSeconds(durationMs)
33+
DURATION: formatDurationSeconds(durationMs),
2634
};
35+
delete env.STATUS;
36+
delete env.FAIL_REASON;
37+
delete env.BODY;
38+
delete env.TITLE;
39+
40+
if (context.status) {
41+
env.STATUS = context.status;
42+
}
43+
if (context.failReason) {
44+
env.FAIL_REASON = context.failReason;
45+
}
46+
if (context.body) {
47+
env.BODY = context.body;
48+
}
49+
if (context.title) {
50+
env.TITLE = context.title;
51+
}
52+
return env;
2753
}
2854

2955
export function launchNotifyScript(
3056
notifyPath: string | undefined,
3157
durationMs: number,
3258
workingDirectory?: string,
3359
spawnProcess: NotifySpawn = spawn as unknown as NotifySpawn,
34-
configuredEnv: Record<string, string> = {}
60+
configuredEnv: Record<string, string> = {},
61+
context: NotifyContext = {}
3562
): void {
3663
const commandPath = notifyPath?.trim();
3764
if (!commandPath) {
@@ -41,8 +68,8 @@ export function launchNotifyScript(
4168
const options = {
4269
cwd: workingDirectory,
4370
detached: process.platform !== "win32",
44-
env: buildNotifyEnv(durationMs, { ...process.env, ...configuredEnv }),
45-
stdio: "ignore" as const
71+
env: buildNotifyEnv(durationMs, { ...process.env, ...configuredEnv }, context),
72+
stdio: "ignore" as const,
4673
};
4774

4875
try {

src/common/openai-thinking.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ export function buildThinkingRequestOptions(
2020

2121
return {
2222
thinking,
23-
...(thinkingEnabled ? { extra_body: { reasoning_effort: reasoningEffort } } : {})
23+
...(thinkingEnabled ? { extra_body: { reasoning_effort: reasoningEffort } } : {}),
2424
};
2525
}

0 commit comments

Comments
 (0)