@ksw8954/git-ai-commit 1.1.0 → 1.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/Makefile +47 -18
  3. package/dist/commands/ai.d.ts +1 -1
  4. package/dist/commands/ai.d.ts.map +1 -1
  5. package/dist/commands/ai.js +10 -3
  6. package/dist/commands/ai.js.map +1 -1
  7. package/dist/commands/commit.d.ts.map +1 -1
  8. package/dist/commands/commit.js +8 -0
  9. package/dist/commands/commit.js.map +1 -1
  10. package/dist/commands/completion.d.ts.map +1 -1
  11. package/dist/commands/completion.js +32 -34
  12. package/dist/commands/completion.js.map +1 -1
  13. package/dist/commands/git.d.ts +1 -0
  14. package/dist/commands/git.d.ts.map +1 -1
  15. package/dist/commands/git.js +20 -0
  16. package/dist/commands/git.js.map +1 -1
  17. package/dist/commands/history.d.ts.map +1 -1
  18. package/dist/commands/history.js +5 -5
  19. package/dist/commands/history.js.map +1 -1
  20. package/dist/commands/log.d.ts +9 -1
  21. package/dist/commands/log.d.ts.map +1 -1
  22. package/dist/commands/log.js +19 -1
  23. package/dist/commands/log.js.map +1 -1
  24. package/dist/commands/prCommand.d.ts.map +1 -1
  25. package/dist/commands/prCommand.js +8 -4
  26. package/dist/commands/prCommand.js.map +1 -1
  27. package/dist/commands/tag.d.ts +1 -0
  28. package/dist/commands/tag.d.ts.map +1 -1
  29. package/dist/commands/tag.js +82 -28
  30. package/dist/commands/tag.js.map +1 -1
  31. package/dist/prompts/tag.d.ts +1 -1
  32. package/dist/prompts/tag.d.ts.map +1 -1
  33. package/dist/prompts/tag.js +35 -3
  34. package/dist/prompts/tag.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/commands/ai.ts +19 -3
  37. package/src/commands/commit.ts +8 -0
  38. package/src/commands/completion.ts +32 -34
  39. package/src/commands/git.ts +20 -0
  40. package/src/commands/history.ts +6 -5
  41. package/src/commands/log.ts +30 -2
  42. package/src/commands/prCommand.ts +8 -4
  43. package/src/commands/tag.ts +98 -30
  44. package/src/prompts/tag.ts +39 -3
@@ -177,6 +177,7 @@ export class CommitCommand {
177
177
  status: "failure",
178
178
  details: diffResult.error,
179
179
  durationMs: Date.now() - start,
180
+ model: mergedModel,
180
181
  });
181
182
  process.exit(1);
182
183
  }
@@ -197,6 +198,7 @@ export class CommitCommand {
197
198
  status: "failure",
198
199
  details: aiResult.error,
199
200
  durationMs: Date.now() - start,
201
+ model: mergedModel,
200
202
  });
201
203
  process.exit(1);
202
204
  }
@@ -214,6 +216,7 @@ export class CommitCommand {
214
216
  status: "success",
215
217
  details: "message-only output",
216
218
  durationMs: Date.now() - start,
219
+ model: mergedModel,
217
220
  });
218
221
  return;
219
222
  }
@@ -231,6 +234,7 @@ export class CommitCommand {
231
234
  status: "cancelled",
232
235
  details: "user declined commit",
233
236
  durationMs: Date.now() - start,
237
+ model: mergedModel,
234
238
  });
235
239
  return;
236
240
  }
@@ -264,6 +268,7 @@ export class CommitCommand {
264
268
  status: "failure",
265
269
  details: "push failed",
266
270
  durationMs: Date.now() - start,
271
+ model: mergedModel,
267
272
  });
268
273
  process.exit(1);
269
274
  }
@@ -276,6 +281,7 @@ export class CommitCommand {
276
281
  status: "failure",
277
282
  details: "git commit failed",
278
283
  durationMs: Date.now() - start,
284
+ model: mergedModel,
279
285
  });
280
286
  process.exit(1);
281
287
  }
@@ -284,6 +290,7 @@ export class CommitCommand {
284
290
  args: safeArgs,
285
291
  status: "success",
286
292
  durationMs: Date.now() - start,
293
+ model: mergedModel,
287
294
  });
288
295
  } catch (error) {
289
296
  const message = error instanceof Error ? error.message : String(error);
@@ -294,6 +301,7 @@ export class CommitCommand {
294
301
  status: "failure",
295
302
  details: message,
296
303
  durationMs: Date.now() - start,
304
+ model: options.model,
297
305
  });
298
306
  process.exit(1);
299
307
  }
@@ -129,20 +129,15 @@ complete -F _git_ai_commit git-ai-commit
129
129
  private generateZshCompletion(): string {
130
130
  return `#compdef git-ai-commit
131
131
  # git-ai-commit zsh completion
132
- # Add to ~/.zshrc:
133
- # eval "$(git-ai-commit completion zsh)"
134
- # Or save to a file in your $fpath (e.g., ~/.zsh/completions/_git-ai-commit)
132
+ # Installation:
133
+ # mkdir -p ~/.zsh/completions
134
+ # git-ai-commit completion zsh > ~/.zsh/completions/_git-ai-commit
135
+ # # Add to ~/.zshrc (before compinit): fpath=(~/.zsh/completions \$fpath)
136
+ # # Then restart shell or run: autoload -Uz compinit && compinit
135
137
 
136
138
  _git-ai-commit() {
137
- local -a commands
138
- commands=(
139
- 'commit:Generate AI-powered commit message'
140
- 'config:Manage git-ai-commit configuration'
141
- 'pr:Generate a pull request title and summary'
142
- 'tag:Create an annotated git tag with AI-generated notes'
143
- 'history:Manage git-ai-commit command history'
144
- 'completion:Generate shell completion scripts'
145
- )
139
+ local curcontext="\$curcontext" state line
140
+ typeset -A opt_args
146
141
 
147
142
  _arguments -C \\
148
143
  '-v[output the version number]' \\
@@ -150,14 +145,23 @@ _git-ai-commit() {
150
145
  '-h[display help]' \\
151
146
  '--help[display help]' \\
152
147
  '1: :->command' \\
153
- '*:: :->args'
148
+ '*:: :->args' && return
154
149
 
155
150
  case \$state in
156
151
  command)
152
+ local -a commands
153
+ commands=(
154
+ 'commit:Generate AI-powered commit message'
155
+ 'config:Manage git-ai-commit configuration'
156
+ 'pr:Generate a pull request title and summary'
157
+ 'tag:Create an annotated git tag with AI-generated notes'
158
+ 'history:Manage git-ai-commit command history'
159
+ 'completion:Generate shell completion scripts'
160
+ )
157
161
  _describe -t commands 'git-ai-commit commands' commands
158
162
  ;;
159
163
  args)
160
- case \$words[1] in
164
+ case \$line[1] in
161
165
  commit)
162
166
  _arguments \\
163
167
  '-k[OpenAI API key]:key:' \\
@@ -190,13 +194,18 @@ _git-ai-commit() {
190
194
  ;;
191
195
  pr)
192
196
  _arguments \\
193
- '--base[Base branch to diff against]:branch:__git_branch_names' \\
194
- '--compare[Compare branch to describe]:branch:__git_branch_names' \\
197
+ '--base[Base branch to diff against]:branch:->branches' \\
198
+ '--compare[Compare branch to describe]:branch:->branches' \\
195
199
  '-k[Override API key]:key:' \\
196
200
  '--api-key[Override API key]:key:' \\
197
201
  '-b[Override API base URL]:url:' \\
198
202
  '--base-url[Override API base URL]:url:' \\
199
203
  '--model[Override AI model]:model:'
204
+ [[ \$state == branches ]] && {
205
+ local -a branches
206
+ branches=(\${(f)"\$(git branch --format='%(refname:short)' 2>/dev/null)"})
207
+ _describe -t branches 'branches' branches
208
+ }
200
209
  ;;
201
210
  tag)
202
211
  _arguments \\
@@ -207,9 +216,14 @@ _git-ai-commit() {
207
216
  '-m[Model to use]:model:' \\
208
217
  '--model[Model to use]:model:' \\
209
218
  '--message[Tag message to use directly]:message:' \\
210
- '-t[Existing tag to diff against]:tag:__git_tags' \\
211
- '--base-tag[Existing tag to diff against]:tag:__git_tags' \\
219
+ '-t[Existing tag to diff against]:tag:->tags' \\
220
+ '--base-tag[Existing tag to diff against]:tag:->tags' \\
212
221
  '--prompt[Additional AI prompt instructions]:text:'
222
+ [[ \$state == tags ]] && {
223
+ local -a tags
224
+ tags=(\${(f)"\$(git tag 2>/dev/null)"})
225
+ _describe -t tags 'tags' tags
226
+ }
213
227
  ;;
214
228
  history)
215
229
  _arguments \\
@@ -226,22 +240,6 @@ _git-ai-commit() {
226
240
  ;;
227
241
  esac
228
242
  }
229
-
230
- # Helper function to complete git branches
231
- __git_branch_names() {
232
- local -a branches
233
- branches=(\${(f)"\$(git branch --format='%(refname:short)' 2>/dev/null)"})
234
- _describe -t branches 'branches' branches
235
- }
236
-
237
- # Helper function to complete git tags
238
- __git_tags() {
239
- local -a tags
240
- tags=(\${(f)"\$(git tag 2>/dev/null)"})
241
- _describe -t tags 'tags' tags
242
- }
243
-
244
- _git-ai-commit "\$@"
245
243
  `;
246
244
  }
247
245
 
@@ -264,6 +264,26 @@ export class GitService {
264
264
  }
265
265
  }
266
266
 
267
+ static async getTagMessage(tagName: string): Promise<string | null> {
268
+ try {
269
+ // Get the tag object content
270
+ const { stdout } = await execFileAsync('git', ['tag', '-l', '-n999', tagName]);
271
+ if (!stdout.trim()) {
272
+ return null;
273
+ }
274
+ // Format: "tagname message line 1\n message line 2..."
275
+ // Remove the tag name prefix and clean up
276
+ const lines = stdout.split('\n');
277
+ const firstLine = lines[0] || '';
278
+ // Remove tag name from first line
279
+ const messageStart = firstLine.replace(new RegExp(`^${tagName}\\s*`), '');
280
+ const restLines = lines.slice(1).map(line => line.replace(/^\s{12}/, ''));
281
+ return [messageStart, ...restLines].join('\n').trim() || null;
282
+ } catch {
283
+ return null;
284
+ }
285
+ }
286
+
267
287
  static async remoteTagExists(tagName: string): Promise<boolean> {
268
288
  try {
269
289
  const { stdout } = await execAsync(`git ls-remote --tags origin refs/tags/${tagName}`);
@@ -38,11 +38,12 @@ export class HistoryCommand {
38
38
 
39
39
  private formatLine(e: any): string {
40
40
  const ts = new Date(e.timestamp).toISOString();
41
- const args = Object.entries(e.args || {})
42
- .filter(([k, v]) => v !== undefined && v !== null && v !== '')
43
- .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
44
- .join(' ');
45
- return `${ts} ${e.command} ${e.status}${e.durationMs ? ` (${e.durationMs}ms)` : ''}${args ? ` -- ${args}` : ''}${e.details ? `\n > ${e.details}` : ''}`;
41
+ const projectName = e.project?.name || 'unknown';
42
+ const model = e.model || 'default';
43
+ const duration = e.durationMs ? ` (${e.durationMs}ms)` : '';
44
+ const details = e.details ? `\n > ${e.details}` : '';
45
+
46
+ return `${ts} [${projectName}] ${e.command} ${e.status}${duration} model=${model}${details}`;
46
47
  }
47
48
 
48
49
  private async handleHistory(options: HistoryOptions) {
@@ -1,9 +1,16 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import { execSync } from 'child_process';
4
5
 
5
6
  export type LogStatus = 'success' | 'failure' | 'cancelled';
6
7
 
8
+ export interface ProjectInfo {
9
+ name: string;
10
+ path: string;
11
+ gitRemote?: string;
12
+ }
13
+
7
14
  export interface HistoryEntry {
8
15
  id: string;
9
16
  timestamp: string; // ISO
@@ -12,6 +19,8 @@ export interface HistoryEntry {
12
19
  status: LogStatus;
13
20
  details?: string;
14
21
  durationMs?: number;
22
+ project?: ProjectInfo;
23
+ model?: string;
15
24
  }
16
25
 
17
26
  function getStorageDir(): string {
@@ -27,8 +36,25 @@ function genId(): string {
27
36
  return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
28
37
  }
29
38
 
39
+ function getProjectInfo(): ProjectInfo {
40
+ const cwd = process.cwd();
41
+ const name = path.basename(cwd);
42
+
43
+ let gitRemote: string | undefined;
44
+ try {
45
+ gitRemote = execSync('git config --get remote.origin.url', {
46
+ encoding: 'utf-8',
47
+ stdio: ['pipe', 'pipe', 'pipe']
48
+ }).trim() || undefined;
49
+ } catch {
50
+ // Not a git repo or no remote
51
+ }
52
+
53
+ return { name, path: cwd, gitRemote };
54
+ }
55
+
30
56
  export class LogService {
31
- static async append(entry: Omit<HistoryEntry, 'id' | 'timestamp'> & { timestamp?: string; id?: string }): Promise<void> {
57
+ static async append(entry: Omit<HistoryEntry, 'id' | 'timestamp' | 'project'> & { timestamp?: string; id?: string; model?: string }): Promise<void> {
32
58
  const file = getHistoryPath();
33
59
  const dir = path.dirname(file);
34
60
  await fs.mkdir(dir, { recursive: true });
@@ -40,7 +66,9 @@ export class LogService {
40
66
  args: entry.args,
41
67
  status: entry.status,
42
68
  details: entry.details,
43
- durationMs: entry.durationMs
69
+ durationMs: entry.durationMs,
70
+ project: getProjectInfo(),
71
+ model: entry.model
44
72
  };
45
73
 
46
74
  const line = JSON.stringify(finalized) + '\n';
@@ -50,7 +50,8 @@ export class PullRequestCommand {
50
50
  command: 'pr',
51
51
  args: { ...options, apiKey: options.apiKey ? '***' : undefined },
52
52
  status: 'failure',
53
- details: err
53
+ details: err,
54
+ model: mergedModel
54
55
  });
55
56
  process.exit(1);
56
57
  return;
@@ -77,7 +78,8 @@ export class PullRequestCommand {
77
78
  command: 'pr',
78
79
  args: { ...options, apiKey: options.apiKey ? '***' : undefined },
79
80
  status: 'failure',
80
- details: err
81
+ details: err,
82
+ model: mergedModel
81
83
  });
82
84
  process.exit(1);
83
85
  return;
@@ -87,7 +89,8 @@ export class PullRequestCommand {
87
89
  await LogService.append({
88
90
  command: 'pr',
89
91
  args: { ...options, apiKey: options.apiKey ? '***' : undefined },
90
- status: 'success'
92
+ status: 'success',
93
+ model: mergedModel
91
94
  });
92
95
  } catch (error) {
93
96
  const message = error instanceof Error ? error.message : String(error);
@@ -96,7 +99,8 @@ export class PullRequestCommand {
96
99
  command: 'pr',
97
100
  args: { ...options, apiKey: options.apiKey ? '***' : undefined },
98
101
  status: 'failure',
99
- details: message
102
+ details: message,
103
+ model: options.model
100
104
  });
101
105
  process.exit(1);
102
106
  }
@@ -20,18 +20,29 @@ export class TagCommand {
20
20
  constructor() {
21
21
  this.program = new Command('tag')
22
22
  .description('Create an annotated git tag with optional AI-generated notes')
23
- .argument('<name>', 'Tag name to create')
23
+ .argument('[name]', 'Tag name to create (auto-increments patch version if omitted)')
24
24
  .option('-k, --api-key <key>', 'OpenAI API key (overrides env var)')
25
25
  .option('--base-url <url>', 'Custom API base URL (overrides env var)')
26
26
  .option('-m, --model <model>', 'Model to use (overrides env var)')
27
27
  .option('--message <message>', 'Tag message to use directly (skips AI generation)')
28
28
  .option('-t, --base-tag <tag>', 'Existing tag to diff against when generating notes')
29
29
  .option('--prompt <text>', 'Additional instructions to append to the AI prompt for this tag')
30
- .action(async (tagName: string, options: TagOptions) => {
30
+ .action(async (tagName: string | undefined, options: TagOptions) => {
31
31
  await this.handleTag(tagName, options);
32
32
  });
33
33
  }
34
34
 
35
+ private incrementPatchVersion(version: string): string | null {
36
+ // Match semver patterns: v1.2.3, 1.2.3, v1.2.3-beta, etc.
37
+ const match = version.match(/^(v?)(\d+)\.(\d+)\.(\d+)(.*)$/);
38
+ if (!match) {
39
+ return null;
40
+ }
41
+ const [, prefix, major, minor, patch, suffix] = match;
42
+ const newPatch = parseInt(patch, 10) + 1;
43
+ return `${prefix}${major}.${minor}.${newPatch}${suffix}`;
44
+ }
45
+
35
46
  private resolveAIConfig(options: TagOptions): AIServiceConfig {
36
47
  const storedConfig = ConfigService.getConfig();
37
48
 
@@ -52,27 +63,58 @@ export class TagCommand {
52
63
  };
53
64
  }
54
65
 
55
- private async handleTag(tagName: string, options: TagOptions): Promise<void> {
56
- const trimmedName = tagName?.trim();
66
+ private async handleTag(tagName: string | undefined, options: TagOptions): Promise<void> {
67
+ const storedConfig = ConfigService.getConfig();
68
+ const mergedModel = options.model || storedConfig.model;
57
69
 
70
+ let trimmedName = tagName?.trim();
71
+
72
+ // If no tag name provided, auto-increment from latest tag
58
73
  if (!trimmedName) {
59
- console.error('Tag name is required.');
60
- await LogService.append({
61
- command: 'tag',
62
- args: { name: tagName, ...options, apiKey: options.apiKey ? '***' : undefined },
63
- status: 'failure',
64
- details: 'missing tag name'
65
- });
66
- process.exit(1);
67
- return;
74
+ const latestTagResult = await GitService.getLatestTag();
75
+
76
+ if (!latestTagResult.success || !latestTagResult.tag) {
77
+ console.error('No existing tags found. Please provide a tag name explicitly.');
78
+ await LogService.append({
79
+ command: 'tag',
80
+ args: { ...options, apiKey: options.apiKey ? '***' : undefined },
81
+ status: 'failure',
82
+ details: 'no existing tags found for auto-increment',
83
+ model: mergedModel
84
+ });
85
+ process.exit(1);
86
+ return;
87
+ }
88
+
89
+ const newVersion = this.incrementPatchVersion(latestTagResult.tag);
90
+
91
+ if (!newVersion) {
92
+ console.error(`Cannot parse version from tag "${latestTagResult.tag}". Please provide a tag name explicitly.`);
93
+ await LogService.append({
94
+ command: 'tag',
95
+ args: { ...options, apiKey: options.apiKey ? '***' : undefined },
96
+ status: 'failure',
97
+ details: `cannot parse version from tag: ${latestTagResult.tag}`,
98
+ model: mergedModel
99
+ });
100
+ process.exit(1);
101
+ return;
102
+ }
103
+
104
+ console.log(`Latest tag: ${latestTagResult.tag}`);
105
+ console.log(`New tag: ${newVersion}`);
106
+ trimmedName = newVersion;
68
107
  }
69
108
 
70
109
  // Check if tag already exists locally
71
110
  const localTagExists = await GitService.tagExists(trimmedName);
72
111
  let remoteTagExists = false;
73
- let wasTagReplaced = false;
112
+ let previousTagMessage: string | null = null;
74
113
 
75
114
  if (localTagExists) {
115
+ // Get existing tag message before deletion for reference
116
+ previousTagMessage = await GitService.getTagMessage(trimmedName);
117
+
76
118
  console.log(`⚠️ Tag ${trimmedName} already exists locally.`);
77
119
  const shouldDelete = await this.confirmTagDelete(trimmedName);
78
120
 
@@ -82,7 +124,8 @@ export class TagCommand {
82
124
  command: 'tag',
83
125
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
84
126
  status: 'cancelled',
85
- details: 'user declined to replace existing tag'
127
+ details: 'user declined to replace existing tag',
128
+ model: mergedModel
86
129
  });
87
130
  return;
88
131
  }
@@ -103,7 +146,8 @@ export class TagCommand {
103
146
  command: 'tag',
104
147
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
105
148
  status: 'failure',
106
- details: 'remote tag deletion failed'
149
+ details: 'remote tag deletion failed',
150
+ model: mergedModel
107
151
  });
108
152
  process.exit(1);
109
153
  return;
@@ -122,13 +166,13 @@ export class TagCommand {
122
166
  command: 'tag',
123
167
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
124
168
  status: 'failure',
125
- details: 'local tag deletion failed'
169
+ details: 'local tag deletion failed',
170
+ model: mergedModel
126
171
  });
127
172
  process.exit(1);
128
173
  return;
129
174
  }
130
175
  console.log(`✅ Local tag ${trimmedName} deleted`);
131
- wasTagReplaced = true;
132
176
  }
133
177
 
134
178
  let tagMessage = options.message?.trim();
@@ -147,6 +191,15 @@ export class TagCommand {
147
191
  }
148
192
  }
149
193
 
194
+ // Get style reference from base tag (if different from current tag being replaced)
195
+ let styleReferenceMessage: string | null = null;
196
+ if (baseTag && baseTag !== trimmedName) {
197
+ styleReferenceMessage = await GitService.getTagMessage(baseTag);
198
+ if (styleReferenceMessage) {
199
+ console.log(`Using ${baseTag} message as style reference.`);
200
+ }
201
+ }
202
+
150
203
  const historyResult = await GitService.getCommitSummariesSince(baseTag);
151
204
  if (!historyResult.success || !historyResult.log) {
152
205
  console.error('Error:', historyResult.error ?? 'Unable to read commit history.');
@@ -154,7 +207,8 @@ export class TagCommand {
154
207
  command: 'tag',
155
208
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
156
209
  status: 'failure',
157
- details: historyResult.error ?? 'Unable to read commit history.'
210
+ details: historyResult.error ?? 'Unable to read commit history.',
211
+ model: mergedModel
158
212
  });
159
213
  process.exit(1);
160
214
  return;
@@ -171,14 +225,21 @@ export class TagCommand {
171
225
  command: 'tag',
172
226
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
173
227
  status: 'failure',
174
- details: message
228
+ details: message,
229
+ model: mergedModel
175
230
  });
176
231
  process.exit(1);
177
232
  return;
178
233
  }
179
234
 
180
235
  const aiService = new AIService(aiConfig);
181
- const aiResult = await aiService.generateTagNotes(trimmedName, historyResult.log, options.prompt);
236
+ const aiResult = await aiService.generateTagNotes(
237
+ trimmedName,
238
+ historyResult.log,
239
+ options.prompt,
240
+ previousTagMessage,
241
+ styleReferenceMessage
242
+ );
182
243
 
183
244
  if (!aiResult.success || !aiResult.notes) {
184
245
  console.error('Error:', aiResult.error ?? 'Failed to generate tag notes.');
@@ -186,7 +247,8 @@ export class TagCommand {
186
247
  command: 'tag',
187
248
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
188
249
  status: 'failure',
189
- details: aiResult.error ?? 'Failed to generate tag notes.'
250
+ details: aiResult.error ?? 'Failed to generate tag notes.',
251
+ model: mergedModel
190
252
  });
191
253
  process.exit(1);
192
254
  return;
@@ -207,7 +269,8 @@ export class TagCommand {
207
269
  command: 'tag',
208
270
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
209
271
  status: 'cancelled',
210
- details: 'user declined tag creation'
272
+ details: 'user declined tag creation',
273
+ model: mergedModel
211
274
  });
212
275
  return;
213
276
  }
@@ -221,7 +284,8 @@ export class TagCommand {
221
284
  command: 'tag',
222
285
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
223
286
  status: 'failure',
224
- details: 'git tag creation failed'
287
+ details: 'git tag creation failed',
288
+ model: mergedModel
225
289
  });
226
290
  process.exit(1);
227
291
  return;
@@ -238,8 +302,8 @@ export class TagCommand {
238
302
  const selectedRemotes = await this.selectRemotesForPush(trimmedName, remotes);
239
303
 
240
304
  if (selectedRemotes && selectedRemotes.length > 0) {
241
- // If tag was replaced or remote tag still exists, use force push
242
- const needsForcePush = wasTagReplaced || remoteTagExists;
305
+ // If remote tag still exists (user declined to delete), use force push
306
+ const needsForcePush = remoteTagExists;
243
307
 
244
308
  if (needsForcePush) {
245
309
  console.log(`\n⚠️ Tag ${trimmedName} may exist on remote. Force push is required.`);
@@ -251,7 +315,8 @@ export class TagCommand {
251
315
  command: 'tag',
252
316
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
253
317
  status: 'cancelled',
254
- details: 'user declined force push'
318
+ details: 'user declined force push',
319
+ model: mergedModel
255
320
  });
256
321
  return;
257
322
  }
@@ -268,7 +333,8 @@ export class TagCommand {
268
333
  command: 'tag',
269
334
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
270
335
  status: 'failure',
271
- details: `tag force push to ${remote} failed`
336
+ details: `tag force push to ${remote} failed`,
337
+ model: mergedModel
272
338
  });
273
339
  }
274
340
  }
@@ -285,7 +351,8 @@ export class TagCommand {
285
351
  command: 'tag',
286
352
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
287
353
  status: 'failure',
288
- details: `tag push to ${remote} failed`
354
+ details: `tag push to ${remote} failed`,
355
+ model: mergedModel
289
356
  });
290
357
  }
291
358
  }
@@ -296,7 +363,8 @@ export class TagCommand {
296
363
  await LogService.append({
297
364
  command: 'tag',
298
365
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
299
- status: 'success'
366
+ status: 'success',
367
+ model: mergedModel
300
368
  });
301
369
  }
302
370
 
@@ -3,7 +3,9 @@ export type TagPromptLanguage = 'ko' | 'en';
3
3
  export const generateTagPrompt = (
4
4
  tagName: string,
5
5
  customInstructions = '',
6
- language: TagPromptLanguage = 'ko'
6
+ language: TagPromptLanguage = 'ko',
7
+ isImprovement = false,
8
+ hasStyleReference = false
7
9
  ): string => {
8
10
  const titleInstruction = language === 'ko'
9
11
  ? `첫 줄에 버전 "${tagName}"을 제목으로 작성하세요.`
@@ -51,15 +53,49 @@ export const generateTagPrompt = (
51
53
  ? '릴리즈 노트를 한국어로 작성하세요.'
52
54
  : 'Write the release notes in English.';
53
55
 
56
+ const improvementInstruction = isImprovement
57
+ ? language === 'ko'
58
+ ? `\n## 개선 모드
59
+ 이전 릴리즈 노트가 제공됩니다. 이를 참고하여:
60
+ - 기존 내용의 구조와 스타일을 유지하면서 개선하세요.
61
+ - 누락된 변경사항이 있으면 추가하세요.
62
+ - 불필요하거나 중복된 내용은 제거하세요.
63
+ - 더 명확하고 사용자 친화적인 표현으로 다듬으세요.\n`
64
+ : `\n## Improvement Mode
65
+ Previous release notes are provided. Use them as reference to:
66
+ - Maintain the structure and style while improving the content.
67
+ - Add any missing changes from the commit log.
68
+ - Remove unnecessary or redundant content.
69
+ - Refine the language to be clearer and more user-friendly.\n`
70
+ : '';
71
+
72
+ const styleReferenceInstruction = hasStyleReference && !isImprovement
73
+ ? language === 'ko'
74
+ ? `\n## 스타일 참조
75
+ 이전 태그의 릴리즈 노트가 스타일 참조용으로 제공됩니다.
76
+ - 제공된 형식과 구조를 따르세요.
77
+ - 제목, 카테고리, 항목 스타일을 일관되게 유지하세요.
78
+ - 언어 톤과 상세도 수준을 맞추세요.\n`
79
+ : `\n## Style Reference
80
+ A previous tag's release notes are provided as style reference.
81
+ - Follow the provided format and structure.
82
+ - Maintain consistency in title, categories, and item styles.
83
+ - Match the language tone and level of detail.\n`
84
+ : '';
85
+
54
86
  return `You are an experienced release manager. Produce clear, user-facing release notes in GitHub Release style.
55
87
 
56
88
  ## Objective
57
- Create release notes for ${tagName} that describe the meaningful changes since the previous release.
89
+ ${isImprovement
90
+ ? `Improve the existing release notes for ${tagName} based on the commit history and previous notes.`
91
+ : `Create release notes for ${tagName} that describe the meaningful changes since the previous release.`}
58
92
 
59
93
  ## Input Context
60
94
  - Target tag to publish: ${tagName}
61
95
  - Commit history between the previous tag and ${tagName} will be supplied in the user message.
62
-
96
+ ${isImprovement ? '- Previous release notes are provided for improvement.' : ''}
97
+ ${hasStyleReference && !isImprovement ? '- Style reference from previous tag is provided.' : ''}
98
+ ${improvementInstruction}${styleReferenceInstruction}
63
99
  ${customInstructions ? `## Additional Instructions\n${customInstructions}\n` : ''}
64
100
  ## Output Format (GitHub Release Style)
65
101
  ${titleInstruction}