@andre-barbosa/opencode-commit 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +112 -127
- package/commands/commit.md +5 -5
- package/opencode.example.json +12 -17
- package/package.json +50 -51
- package/src/generate.ts +89 -112
- package/src/git.ts +116 -64
- package/src/index.ts +149 -136
- package/src/prompt.ts +72 -51
- package/src/types.ts +53 -40
- package/agents/commit-writer.md +0 -27
package/src/git.ts
CHANGED
|
@@ -1,64 +1,116 @@
|
|
|
1
|
-
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
-
import type { GitContext } from "./types.js";
|
|
3
|
-
|
|
4
|
-
type Shell = PluginInput["$"];
|
|
5
|
-
|
|
6
|
-
async function readGitOutput(
|
|
7
|
-
$: Shell,
|
|
8
|
-
worktree: string,
|
|
9
|
-
run: ($: Shell) => Promise<{ exitCode: number; stdout?: { toString(): string } }>,
|
|
10
|
-
): Promise<string> {
|
|
11
|
-
const result = await run($);
|
|
12
|
-
if (result.exitCode !== 0) {
|
|
13
|
-
return "";
|
|
14
|
-
}
|
|
15
|
-
return result.stdout?.toString().trim() ?? "";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function isGitRepo($: Shell, worktree: string): Promise<boolean> {
|
|
19
|
-
const result = await $`git rev-parse --git-dir`.cwd(worktree).quiet().nothrow();
|
|
20
|
-
return result.exitCode === 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import type { GitContext } from "./types.js";
|
|
3
|
+
|
|
4
|
+
type Shell = PluginInput["$"];
|
|
5
|
+
|
|
6
|
+
async function readGitOutput(
|
|
7
|
+
$: Shell,
|
|
8
|
+
worktree: string,
|
|
9
|
+
run: ($: Shell) => Promise<{ exitCode: number; stdout?: { toString(): string } }>,
|
|
10
|
+
): Promise<string> {
|
|
11
|
+
const result = await run($);
|
|
12
|
+
if (result.exitCode !== 0) {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
return result.stdout?.toString().trim() ?? "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function isGitRepo($: Shell, worktree: string): Promise<boolean> {
|
|
19
|
+
const result = await $`git rev-parse --git-dir`.cwd(worktree).quiet().nothrow();
|
|
20
|
+
return result.exitCode === 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function truncateDiff(diff: string, maxChars: number, label: string): string {
|
|
24
|
+
if (diff.length <= maxChars) return diff;
|
|
25
|
+
return `${diff.slice(0, maxChars)}\n\n[${label} diff truncated at ${maxChars} characters]`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function splitDiffs(input: {
|
|
29
|
+
staged: string;
|
|
30
|
+
unstaged: string;
|
|
31
|
+
maxDiffChars: number;
|
|
32
|
+
}): { staged: string; unstaged: string } {
|
|
33
|
+
if (!input.staged || !input.unstaged) {
|
|
34
|
+
return {
|
|
35
|
+
staged: truncateDiff(input.staged, input.maxDiffChars, "staged"),
|
|
36
|
+
unstaged: truncateDiff(input.unstaged, input.maxDiffChars, "unstaged"),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const half = Math.floor(input.maxDiffChars / 2);
|
|
41
|
+
let stagedLimit = Math.min(input.staged.length, half);
|
|
42
|
+
let unstagedLimit = Math.min(
|
|
43
|
+
input.unstaged.length,
|
|
44
|
+
input.maxDiffChars - stagedLimit,
|
|
45
|
+
);
|
|
46
|
+
stagedLimit = Math.min(input.staged.length, input.maxDiffChars - unstagedLimit);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
staged: truncateDiff(input.staged, stagedLimit, "staged"),
|
|
50
|
+
unstaged: truncateDiff(input.unstaged, unstagedLimit, "unstaged"),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function gatherGitContext(
|
|
55
|
+
$: Shell,
|
|
56
|
+
worktree: string,
|
|
57
|
+
maxDiffChars: number,
|
|
58
|
+
): Promise<GitContext> {
|
|
59
|
+
const branch =
|
|
60
|
+
(await readGitOutput($, worktree, ($) =>
|
|
61
|
+
$`git symbolic-ref --quiet --short HEAD`.cwd(worktree).quiet().nothrow(),
|
|
62
|
+
)) ||
|
|
63
|
+
(await readGitOutput($, worktree, ($) =>
|
|
64
|
+
$`git rev-parse --short HEAD`.cwd(worktree).quiet().nothrow(),
|
|
65
|
+
)) ||
|
|
66
|
+
"unknown";
|
|
67
|
+
|
|
68
|
+
const stagedStat = await readGitOutput($, worktree, ($) =>
|
|
69
|
+
$`git diff --staged --stat`.cwd(worktree).quiet().nothrow(),
|
|
70
|
+
);
|
|
71
|
+
const rawStagedDiff = await readGitOutput($, worktree, ($) =>
|
|
72
|
+
$`git diff --staged`.cwd(worktree).quiet().nothrow(),
|
|
73
|
+
);
|
|
74
|
+
const unstagedStat = await readGitOutput($, worktree, ($) =>
|
|
75
|
+
$`git diff --stat`.cwd(worktree).quiet().nothrow(),
|
|
76
|
+
);
|
|
77
|
+
const rawUnstagedDiff = await readGitOutput($, worktree, ($) =>
|
|
78
|
+
$`git diff`.cwd(worktree).quiet().nothrow(),
|
|
79
|
+
);
|
|
80
|
+
const untrackedOutput = await readGitOutput($, worktree, ($) =>
|
|
81
|
+
$`git ls-files --others --exclude-standard`.cwd(worktree).quiet().nothrow(),
|
|
82
|
+
);
|
|
83
|
+
const untrackedFiles = untrackedOutput
|
|
84
|
+
? untrackedOutput.split("\n").map((line) => line.trim()).filter(Boolean)
|
|
85
|
+
: [];
|
|
86
|
+
const diffs = splitDiffs({
|
|
87
|
+
staged: rawStagedDiff,
|
|
88
|
+
unstaged: rawUnstagedDiff,
|
|
89
|
+
maxDiffChars,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const logOutput = await readGitOutput($, worktree, ($) =>
|
|
93
|
+
$`git log -5 --pretty=format:%s`.cwd(worktree).quiet().nothrow(),
|
|
94
|
+
);
|
|
95
|
+
const recentCommits = logOutput
|
|
96
|
+
? logOutput.split("\n").map((line) => line.trim()).filter(Boolean)
|
|
97
|
+
: [];
|
|
98
|
+
|
|
99
|
+
const hasUncommittedChanges =
|
|
100
|
+
stagedStat.length > 0 ||
|
|
101
|
+
rawStagedDiff.length > 0 ||
|
|
102
|
+
unstagedStat.length > 0 ||
|
|
103
|
+
rawUnstagedDiff.length > 0 ||
|
|
104
|
+
untrackedFiles.length > 0;
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
branch,
|
|
108
|
+
stagedStat,
|
|
109
|
+
stagedDiff: diffs.staged,
|
|
110
|
+
unstagedStat,
|
|
111
|
+
unstagedDiff: diffs.unstaged,
|
|
112
|
+
untrackedFiles,
|
|
113
|
+
recentCommits,
|
|
114
|
+
hasUncommittedChanges,
|
|
115
|
+
};
|
|
116
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,136 +1,149 @@
|
|
|
1
|
-
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
-
import type { Part } from "@opencode-ai/sdk";
|
|
3
|
-
import { gatherGitContext, isGitRepo } from "./git.js";
|
|
4
|
-
import {
|
|
5
|
-
import { resolveConfig, type PluginOptions } from "./types.js";
|
|
6
|
-
|
|
7
|
-
const COMMAND_NAME = "commit";
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
"```",
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"",
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
import type { Part } from "@opencode-ai/sdk";
|
|
3
|
+
import { gatherGitContext, isGitRepo } from "./git.js";
|
|
4
|
+
import { generateCommitMessage } from "./generate.js";
|
|
5
|
+
import { parseModelRef, resolveConfig, type PluginOptions } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const COMMAND_NAME = "commit";
|
|
8
|
+
const COMMAND_DESCRIPTION = "Generate a commit message from uncommitted changes";
|
|
9
|
+
const COMMAND_TEMPLATE = "Handled by the opencode-commit plugin.";
|
|
10
|
+
const SKIP_ERROR = "skip";
|
|
11
|
+
|
|
12
|
+
function textPart(text: string): Part {
|
|
13
|
+
return { type: "text", text } as Part;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatOutput(input: {
|
|
17
|
+
message: string;
|
|
18
|
+
modelLabel: string;
|
|
19
|
+
childSessionID: string;
|
|
20
|
+
}): string {
|
|
21
|
+
return [
|
|
22
|
+
"## Suggested commit message",
|
|
23
|
+
"",
|
|
24
|
+
"```",
|
|
25
|
+
input.message,
|
|
26
|
+
"```",
|
|
27
|
+
"",
|
|
28
|
+
`Model: \`${input.modelLabel}\` · Child session: \`${input.childSessionID}\``,
|
|
29
|
+
"",
|
|
30
|
+
"Copy the message above and run `git commit` when ready.",
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const skipCommand = (): never => {
|
|
35
|
+
throw new Error(SKIP_ERROR);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const OpenCodeCommitPlugin: Plugin = async (
|
|
39
|
+
{ client, $, worktree },
|
|
40
|
+
options?: PluginOptions,
|
|
41
|
+
) => {
|
|
42
|
+
const config = resolveConfig(options);
|
|
43
|
+
|
|
44
|
+
const log = async (
|
|
45
|
+
level: "info" | "warn" | "error",
|
|
46
|
+
message: string,
|
|
47
|
+
extra?: Record<string, unknown>,
|
|
48
|
+
) => {
|
|
49
|
+
await client.app
|
|
50
|
+
.log({
|
|
51
|
+
body: {
|
|
52
|
+
service: "opencode-commit",
|
|
53
|
+
level,
|
|
54
|
+
message,
|
|
55
|
+
extra,
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
.catch(() => {});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
config: async (cfg) => {
|
|
63
|
+
cfg.command ??= {};
|
|
64
|
+
cfg.command[COMMAND_NAME] ??= {
|
|
65
|
+
description: COMMAND_DESCRIPTION,
|
|
66
|
+
template: COMMAND_TEMPLATE,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (config.model && !cfg.command[COMMAND_NAME].model) {
|
|
70
|
+
cfg.command[COMMAND_NAME].model = config.model;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"command.execute.before": async (input, output) => {
|
|
74
|
+
if (input.command !== COMMAND_NAME) return;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
if (!config.model) {
|
|
78
|
+
output.parts = [
|
|
79
|
+
textPart(
|
|
80
|
+
[
|
|
81
|
+
"No commit model configured.",
|
|
82
|
+
"",
|
|
83
|
+
"Add a `model` option to the opencode-commit plugin config, for example:",
|
|
84
|
+
"`[\"@andre-barbosa/opencode-commit\", { \"model\": \"opencode-go/deepseek-v4-flash\" }]`",
|
|
85
|
+
].join("\n"),
|
|
86
|
+
),
|
|
87
|
+
];
|
|
88
|
+
return skipCommand();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const model = parseModelRef(config.model);
|
|
92
|
+
|
|
93
|
+
if (!(await isGitRepo($, worktree))) {
|
|
94
|
+
output.parts = [
|
|
95
|
+
textPart(
|
|
96
|
+
"Not a git repository. Run `/commit` from inside a git worktree.",
|
|
97
|
+
),
|
|
98
|
+
];
|
|
99
|
+
return skipCommand();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const context = await gatherGitContext($, worktree, config.maxDiffChars);
|
|
103
|
+
|
|
104
|
+
if (!context.hasUncommittedChanges) {
|
|
105
|
+
output.parts = [
|
|
106
|
+
textPart(
|
|
107
|
+
"No uncommitted changes found. Make changes and run `/commit` again.",
|
|
108
|
+
),
|
|
109
|
+
];
|
|
110
|
+
return skipCommand();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await log("info", "Generating commit message", {
|
|
114
|
+
model: config.model,
|
|
115
|
+
branch: context.branch,
|
|
116
|
+
sessionID: input.sessionID,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const userHint = input.arguments?.trim() || undefined;
|
|
120
|
+
const result = await generateCommitMessage({
|
|
121
|
+
client,
|
|
122
|
+
parentSessionID: input.sessionID,
|
|
123
|
+
model,
|
|
124
|
+
context,
|
|
125
|
+
userHint,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
output.parts = [textPart(formatOutput(result))];
|
|
129
|
+
return skipCommand();
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error instanceof Error && error.message === SKIP_ERROR) {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await log("error", "Commit message generation failed", {
|
|
136
|
+
error: String(error),
|
|
137
|
+
sessionID: input.sessionID,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
output.parts = [
|
|
141
|
+
textPart(`Failed to generate commit message: ${String(error)}`),
|
|
142
|
+
];
|
|
143
|
+
return skipCommand();
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export default OpenCodeCommitPlugin;
|
package/src/prompt.ts
CHANGED
|
@@ -1,51 +1,72 @@
|
|
|
1
|
-
import type { GitContext } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
import type { GitContext } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const COMMIT_SYSTEM_PROMPT = [
|
|
4
|
+
"You are a commit message writer.",
|
|
5
|
+
"Given git changes and context, produce a single commit message.",
|
|
6
|
+
"Output only the commit message text.",
|
|
7
|
+
].join("\n");
|
|
8
|
+
|
|
9
|
+
export function buildCommitPrompt(context: GitContext, userHint?: string): string {
|
|
10
|
+
const recentCommitsBlock =
|
|
11
|
+
context.recentCommits.length > 0
|
|
12
|
+
? context.recentCommits.map((subject) => `- ${subject}`).join("\n")
|
|
13
|
+
: "(none)";
|
|
14
|
+
const untrackedFilesBlock =
|
|
15
|
+
context.untrackedFiles.length > 0
|
|
16
|
+
? context.untrackedFiles.map((file) => `- ${file}`).join("\n")
|
|
17
|
+
: "(none)";
|
|
18
|
+
|
|
19
|
+
const hintBlock =
|
|
20
|
+
userHint && userHint.trim().length > 0
|
|
21
|
+
? `\nAdditional instruction from the user:\n${userHint.trim()}\n`
|
|
22
|
+
: "";
|
|
23
|
+
|
|
24
|
+
return `Write a git commit message for the uncommitted changes below.
|
|
25
|
+
|
|
26
|
+
Format rules (Conventional Commits - required):
|
|
27
|
+
- Subject line MUST use type(scope): description or type: description
|
|
28
|
+
- Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
|
29
|
+
- Scope is optional but preferred when changes are localized (e.g. feat(auth):, fix(api):)
|
|
30
|
+
- Description: imperative mood, lowercase, no trailing period, max 72 chars for the full subject
|
|
31
|
+
- Optional body: blank line after subject, then bullet points; keep lines <= 72 chars
|
|
32
|
+
- Breaking changes: use feat!: or fix!: prefix, or add a BREAKING CHANGE: footer
|
|
33
|
+
- Do NOT use free-form subjects like "Update files", "WIP", or "Misc changes"
|
|
34
|
+
- Use staged and unstaged tracked diffs as the source of truth
|
|
35
|
+
- Untracked files are filenames only; do not infer contents that are not shown
|
|
36
|
+
- Output ONLY the commit message text - no code fences, no commentary, no tool calls
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
feat(auth): add oauth login flow
|
|
40
|
+
|
|
41
|
+
- add google and github providers
|
|
42
|
+
- store refresh tokens in secure storage
|
|
43
|
+
|
|
44
|
+
fix(api): handle null response from user endpoint
|
|
45
|
+
${hintBlock}
|
|
46
|
+
Branch: ${context.branch}
|
|
47
|
+
|
|
48
|
+
Recent commit subjects (align scope naming when sensible):
|
|
49
|
+
${recentCommitsBlock}
|
|
50
|
+
|
|
51
|
+
Staged tracked changes (stat):
|
|
52
|
+
${context.stagedStat || "(empty)"}
|
|
53
|
+
|
|
54
|
+
Staged tracked diff:
|
|
55
|
+
${context.stagedDiff || "(empty)"}
|
|
56
|
+
|
|
57
|
+
Unstaged tracked changes (stat):
|
|
58
|
+
${context.unstagedStat || "(empty)"}
|
|
59
|
+
|
|
60
|
+
Unstaged tracked diff:
|
|
61
|
+
${context.unstagedDiff || "(empty)"}
|
|
62
|
+
|
|
63
|
+
Untracked files (filenames only, contents not read):
|
|
64
|
+
${untrackedFilesBlock}
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function stripCodeFences(text: string): string {
|
|
69
|
+
const trimmed = text.trim();
|
|
70
|
+
const fenced = trimmed.match(/^```(?:\w*\n)?([\s\S]*?)```$/);
|
|
71
|
+
return (fenced?.[1] ?? trimmed).trim();
|
|
72
|
+
}
|