gut-cli 0.1.19 → 0.1.21

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.
@@ -9,11 +9,17 @@ declare function listProviders(): Promise<{
9
9
  hasKey: boolean;
10
10
  }[]>;
11
11
 
12
+ type Language = 'en' | 'ja';
13
+ declare function getLanguage(): Language;
14
+ declare function setLanguage(lang: Language, local?: boolean): void;
15
+ declare function getLanguageInstruction(lang: Language): string;
16
+
12
17
  interface AIOptions {
13
18
  provider: Provider;
14
19
  model?: string;
15
20
  ollamaBaseUrl?: string;
16
21
  apiKey?: string;
22
+ language?: Language;
17
23
  }
18
24
  /**
19
25
  * Find a user's project template from .gut/ folder
@@ -292,9 +298,4 @@ declare function generateGitignore(context: {
292
298
  existingGitignore?: string;
293
299
  }, options: AIOptions, template?: string): Promise<string>;
294
300
 
295
- type Language = 'en' | 'ja';
296
- declare function getLanguage(): Language;
297
- declare function setLanguage(lang: Language, local?: boolean): void;
298
- declare function getLanguageInstruction(lang: Language): string;
299
-
300
- export { type AIOptions, type Changelog, type CodeReview, type CommitSearchResult, type ConflictResolution, type Explanation, type Provider, type WorkSummary, deleteApiKey, findTemplate, generateBranchName, generateBranchNameFromDiff, generateChangelog, generateCodeReview, generateCommitMessage, generateExplanation, generateGitignore, generatePRDescription, generateStashName, generateWorkSummary, getApiKey, getLanguage, getLanguageInstruction, listProviders, resolveConflict, saveApiKey, searchCommits, setLanguage };
301
+ export { type AIOptions, type Changelog, type CodeReview, type CommitSearchResult, type ConflictResolution, type Explanation, type Language, type Provider, type WorkSummary, deleteApiKey, findTemplate, generateBranchName, generateBranchNameFromDiff, generateChangelog, generateCodeReview, generateCommitMessage, generateExplanation, generateGitignore, generatePRDescription, generateStashName, generateWorkSummary, getApiKey, getLanguage, getLanguageInstruction, listProviders, resolveConflict, saveApiKey, searchCommits, setLanguage };
package/dist/lib/index.js CHANGED
@@ -5,9 +5,9 @@ import { createOpenAI } from "@ai-sdk/openai";
5
5
  import { createAnthropic } from "@ai-sdk/anthropic";
6
6
  import { createOllama } from "ollama-ai-provider";
7
7
  import { z } from "zod";
8
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
9
- import { join as join2, dirname } from "path";
10
- import { homedir as homedir2 } from "os";
8
+ import { existsSync, readFileSync } from "fs";
9
+ import { join, dirname } from "path";
10
+ import { homedir } from "os";
11
11
  import { fileURLToPath } from "url";
12
12
 
13
13
  // src/lib/credentials.ts
@@ -80,138 +80,42 @@ async function listProviders() {
80
80
  return results;
81
81
  }
82
82
 
83
- // src/lib/config.ts
84
- import { homedir } from "os";
85
- import { join } from "path";
86
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
87
- import { execSync } from "child_process";
88
- var DEFAULT_CONFIG = {
89
- lang: "en"
90
- };
91
- function getGlobalConfigPath() {
92
- const configDir = join(homedir(), ".config", "gut");
93
- return join(configDir, "config.json");
94
- }
95
- function getRepoRoot() {
96
- try {
97
- return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
98
- } catch {
99
- return null;
100
- }
101
- }
102
- function getLocalConfigPath() {
103
- const repoRoot = getRepoRoot();
104
- if (!repoRoot) return null;
105
- return join(repoRoot, ".gut", "config.json");
106
- }
107
- function ensureGlobalConfigDir() {
108
- const configDir = join(homedir(), ".config", "gut");
109
- if (!existsSync(configDir)) {
110
- mkdirSync(configDir, { recursive: true });
111
- }
112
- }
113
- function ensureLocalConfigDir() {
114
- const repoRoot = getRepoRoot();
115
- if (!repoRoot) return;
116
- const gutDir = join(repoRoot, ".gut");
117
- if (!existsSync(gutDir)) {
118
- mkdirSync(gutDir, { recursive: true });
119
- }
120
- }
121
- function readConfigFile(path) {
122
- if (!existsSync(path)) return {};
123
- try {
124
- return JSON.parse(readFileSync(path, "utf-8"));
125
- } catch {
126
- return {};
127
- }
128
- }
129
- function getGlobalConfig() {
130
- const globalPath = getGlobalConfigPath();
131
- return { ...DEFAULT_CONFIG, ...readConfigFile(globalPath) };
132
- }
133
- function getLocalConfig() {
134
- const localPath = getLocalConfigPath();
135
- if (!localPath) return {};
136
- return readConfigFile(localPath);
137
- }
138
- function getConfig() {
139
- const globalConfig = getGlobalConfig();
140
- const localConfig = getLocalConfig();
141
- return { ...globalConfig, ...localConfig };
142
- }
143
- function setGlobalConfig(key, value) {
144
- ensureGlobalConfigDir();
145
- const config = getGlobalConfig();
146
- config[key] = value;
147
- writeFileSync(getGlobalConfigPath(), JSON.stringify(config, null, 2));
148
- }
149
- function setLocalConfig(key, value) {
150
- const localPath = getLocalConfigPath();
151
- if (!localPath) {
152
- throw new Error("Not in a git repository");
153
- }
154
- ensureLocalConfigDir();
155
- const config = getLocalConfig();
156
- config[key] = value;
157
- writeFileSync(localPath, JSON.stringify(config, null, 2));
158
- }
159
- function getLanguage() {
160
- return getConfig().lang;
161
- }
162
- function setLanguage(lang, local = false) {
163
- if (local) {
164
- setLocalConfig("lang", lang);
165
- } else {
166
- setGlobalConfig("lang", lang);
167
- }
168
- }
169
- function getLanguageInstruction(lang) {
170
- switch (lang) {
171
- case "ja":
172
- return "\n\nIMPORTANT: Respond in Japanese (\u65E5\u672C\u8A9E\u3067\u56DE\u7B54\u3057\u3066\u304F\u3060\u3055\u3044).";
173
- case "en":
174
- default:
175
- return "";
176
- }
177
- }
178
-
179
83
  // src/lib/ai.ts
180
84
  var __filename = fileURLToPath(import.meta.url);
181
85
  var __dirname = dirname(__filename);
182
86
  function findGutRoot() {
183
87
  let current = __dirname;
184
88
  for (let i = 0; i < 5; i++) {
185
- const gutPath = join2(current, ".gut");
186
- if (existsSync2(gutPath)) {
89
+ const gutPath = join(current, ".gut");
90
+ if (existsSync(gutPath)) {
187
91
  return current;
188
92
  }
189
93
  current = dirname(current);
190
94
  }
191
- return join2(__dirname, "..");
95
+ return join(__dirname, "..");
192
96
  }
193
97
  var GUT_ROOT = findGutRoot();
194
98
  function loadTemplate(name) {
195
- const templatePath = join2(GUT_ROOT, ".gut", `${name}.md`);
196
- if (existsSync2(templatePath)) {
197
- return readFileSync2(templatePath, "utf-8");
99
+ const templatePath = join(GUT_ROOT, ".gut", `${name}.md`);
100
+ if (existsSync(templatePath)) {
101
+ return readFileSync(templatePath, "utf-8");
198
102
  }
199
103
  throw new Error(`Template not found: ${templatePath}`);
200
104
  }
201
105
  function getGlobalTemplatesDir() {
202
- return join2(homedir2(), ".config", "gut", "templates");
106
+ return join(homedir(), ".config", "gut", "templates");
203
107
  }
204
108
  function findGlobalTemplate(templateName) {
205
- const templatePath = join2(getGlobalTemplatesDir(), `${templateName}.md`);
206
- if (existsSync2(templatePath)) {
207
- return readFileSync2(templatePath, "utf-8");
109
+ const templatePath = join(getGlobalTemplatesDir(), `${templateName}.md`);
110
+ if (existsSync(templatePath)) {
111
+ return readFileSync(templatePath, "utf-8");
208
112
  }
209
113
  return null;
210
114
  }
211
115
  function findTemplate(repoRoot, templateName) {
212
- const projectTemplatePath = join2(repoRoot, ".gut", `${templateName}.md`);
213
- if (existsSync2(projectTemplatePath)) {
214
- return readFileSync2(projectTemplatePath, "utf-8");
116
+ const projectTemplatePath = join(repoRoot, ".gut", `${templateName}.md`);
117
+ if (existsSync(projectTemplatePath)) {
118
+ return readFileSync(projectTemplatePath, "utf-8");
215
119
  }
216
120
  const globalTemplate = findGlobalTemplate(templateName);
217
121
  if (globalTemplate) {
@@ -219,19 +123,25 @@ function findTemplate(repoRoot, templateName) {
219
123
  }
220
124
  return null;
221
125
  }
222
- function applyTemplate(userTemplate, templateName, variables) {
223
- const langInstruction = getLanguageInstruction(getLanguage());
224
- let result = userTemplate || loadTemplate(templateName);
225
- result = result.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, content) => {
226
- return variables[key] ? content : "";
227
- });
228
- for (const [key, value] of Object.entries(variables)) {
229
- result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value || "");
230
- }
231
- if (langInstruction) {
232
- result += langInstruction;
126
+ function buildPrompt(userTemplate, templateName, context, language, outputFormat) {
127
+ let contextXml = "<context>\n";
128
+ for (const [key, value] of Object.entries(context)) {
129
+ if (value) {
130
+ contextXml += `<${key}>
131
+ ${value}
132
+ </${key}>
133
+ `;
134
+ }
233
135
  }
234
- return result;
136
+ contextXml += "</context>\n\n";
137
+ const template = userTemplate || loadTemplate(templateName);
138
+ const langInstruction = language === "ja" ? "\n\nIMPORTANT: Respond in Japanese (\u65E5\u672C\u8A9E\u3067\u56DE\u7B54\u3057\u3066\u304F\u3060\u3055\u3044)." : "";
139
+ const outputSection = outputFormat ? `
140
+
141
+ <output-format>
142
+ ${outputFormat}
143
+ </output-format>` : "";
144
+ return contextXml + "<instructions>\n" + template + langInstruction + "\n</instructions>" + outputSection;
235
145
  }
236
146
  var DEFAULT_MODELS = {
237
147
  gemini: "gemini-2.0-flash",
@@ -279,9 +189,9 @@ async function getModel(options) {
279
189
  }
280
190
  async function generateCommitMessage(diff, options, template) {
281
191
  const model = await getModel(options);
282
- const prompt = applyTemplate(template, "commit", {
192
+ const prompt = buildPrompt(template, "commit", {
283
193
  diff: diff.slice(0, 8e3)
284
- });
194
+ }, options.language, "Respond with ONLY the commit message, nothing else.");
285
195
  const result = await generateText({
286
196
  model,
287
197
  prompt,
@@ -291,12 +201,18 @@ async function generateCommitMessage(diff, options, template) {
291
201
  }
292
202
  async function generatePRDescription(context, options, template) {
293
203
  const model = await getModel(options);
294
- const prompt = applyTemplate(template, "pr", {
204
+ const prompt = buildPrompt(template, "pr", {
295
205
  baseBranch: context.baseBranch,
296
206
  currentBranch: context.currentBranch,
297
207
  commits: context.commits.map((c) => `- ${c}`).join("\n"),
298
208
  diff: context.diff.slice(0, 6e3)
299
- });
209
+ }, options.language, `Respond in JSON format:
210
+ \`\`\`json
211
+ {
212
+ "title": "...",
213
+ "body": "..."
214
+ }
215
+ \`\`\``);
300
216
  const result = await generateText({
301
217
  model,
302
218
  prompt,
@@ -327,9 +243,9 @@ var CodeReviewSchema = z.object({
327
243
  });
328
244
  async function generateCodeReview(diff, options, template) {
329
245
  const model = await getModel(options);
330
- const prompt = applyTemplate(template, "review", {
246
+ const prompt = buildPrompt(template, "review", {
331
247
  diff: diff.slice(0, 1e4)
332
- });
248
+ }, options.language);
333
249
  const result = await generateObject({
334
250
  model,
335
251
  schema: CodeReviewSchema,
@@ -351,13 +267,13 @@ var ChangelogSchema = z.object({
351
267
  async function generateChangelog(context, options, template) {
352
268
  const model = await getModel(options);
353
269
  const commitList = context.commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message} (${c.author})`).join("\n");
354
- const prompt = applyTemplate(template, "changelog", {
270
+ const prompt = buildPrompt(template, "changelog", {
355
271
  fromRef: context.fromRef,
356
272
  toRef: context.toRef,
357
273
  commits: commitList,
358
274
  diff: context.diff.slice(0, 8e3),
359
275
  todayDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
360
- });
276
+ }, options.language);
361
277
  const result = await generateObject({
362
278
  model,
363
279
  schema: ChangelogSchema,
@@ -385,10 +301,10 @@ var ExplanationSchema = z.object({
385
301
  async function generateExplanation(context, options, template) {
386
302
  const model = await getModel(options);
387
303
  if (context.type === "file-content") {
388
- const prompt2 = applyTemplate(template, "explain-file", {
304
+ const prompt2 = buildPrompt(template, "explain-file", {
389
305
  filePath: context.metadata.filePath || "",
390
306
  content: context.content?.slice(0, 15e3) || ""
391
- });
307
+ }, options.language);
392
308
  const result2 = await generateObject({
393
309
  model,
394
310
  schema: ExplanationSchema,
@@ -422,11 +338,11 @@ Author: ${context.metadata.author}
422
338
  Date: ${context.metadata.date}`;
423
339
  targetType = "commit";
424
340
  }
425
- const prompt = applyTemplate(template, "explain", {
341
+ const prompt = buildPrompt(template, "explain", {
426
342
  targetType,
427
- context: contextInfo,
343
+ contextInfo,
428
344
  diff: context.diff?.slice(0, 12e3) || ""
429
- });
345
+ }, options.language);
430
346
  const result = await generateObject({
431
347
  model,
432
348
  schema: ExplanationSchema,
@@ -446,11 +362,11 @@ var CommitSearchSchema = z.object({
446
362
  async function searchCommits(query, commits, options, maxResults = 5, template) {
447
363
  const model = await getModel(options);
448
364
  const commitList = commits.map((c) => `${c.hash.slice(0, 7)} | ${c.author} | ${c.date.split("T")[0]} | ${c.message.split("\n")[0]}`).join("\n");
449
- const prompt = applyTemplate(template, "find", {
365
+ const prompt = buildPrompt(template, "find", {
450
366
  query,
451
367
  commits: commitList,
452
368
  maxResults: String(maxResults)
453
- });
369
+ }, options.language);
454
370
  const result = await generateObject({
455
371
  model,
456
372
  schema: CommitSearchSchema,
@@ -479,11 +395,11 @@ async function searchCommits(query, commits, options, maxResults = 5, template)
479
395
  }
480
396
  async function generateBranchName(description, options, context, template) {
481
397
  const model = await getModel(options);
482
- const prompt = applyTemplate(template, "branch", {
398
+ const prompt = buildPrompt(template, "branch", {
483
399
  description,
484
400
  type: context?.type,
485
401
  issue: context?.issue
486
- });
402
+ }, options.language, "Respond with ONLY the branch name, nothing else.");
487
403
  const result = await generateText({
488
404
  model,
489
405
  prompt,
@@ -493,9 +409,9 @@ async function generateBranchName(description, options, context, template) {
493
409
  }
494
410
  async function generateBranchNameFromDiff(diff, options, template) {
495
411
  const model = await getModel(options);
496
- const prompt = applyTemplate(template, "checkout", {
412
+ const prompt = buildPrompt(template, "checkout", {
497
413
  diff: diff.slice(0, 8e3)
498
- });
414
+ }, options.language, "Respond with ONLY the branch name, nothing else.");
499
415
  const result = await generateText({
500
416
  model,
501
417
  prompt,
@@ -505,9 +421,9 @@ async function generateBranchNameFromDiff(diff, options, template) {
505
421
  }
506
422
  async function generateStashName(diff, options, template) {
507
423
  const model = await getModel(options);
508
- const prompt = applyTemplate(template, "stash", {
424
+ const prompt = buildPrompt(template, "stash", {
509
425
  diff: diff.slice(0, 4e3)
510
- });
426
+ }, options.language, "Respond with ONLY the stash name, nothing else.");
511
427
  const result = await generateText({
512
428
  model,
513
429
  prompt,
@@ -537,13 +453,13 @@ async function generateWorkSummary(context, options, format = "custom", template
537
453
  const commitList = context.commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message.split("\n")[0]} (${c.date.split("T")[0]})`).join("\n");
538
454
  const formatHint = format === "daily" ? "This is a daily report. Focus on today's accomplishments." : format === "weekly" ? "This is a weekly report. Summarize the week's work at a higher level." : `This is a summary from ${context.since}${context.until ? ` to ${context.until}` : ""}.`;
539
455
  const period = `${context.since}${context.until ? ` to ${context.until}` : " to now"}`;
540
- const prompt = applyTemplate(template, "summary", {
456
+ const prompt = buildPrompt(template, "summary", {
541
457
  author: context.author,
542
458
  period,
543
459
  format: formatHint,
544
460
  commits: commitList,
545
461
  diff: context.diff?.slice(0, 6e3)
546
- });
462
+ }, options.language);
547
463
  const result = await generateObject({
548
464
  model,
549
465
  schema: WorkSummarySchema,
@@ -559,12 +475,12 @@ async function generateWorkSummary(context, options, format = "custom", template
559
475
  }
560
476
  async function resolveConflict(conflictedContent, context, options, template) {
561
477
  const model = await getModel(options);
562
- const prompt = applyTemplate(template, "merge", {
478
+ const prompt = buildPrompt(template, "merge", {
563
479
  filename: context.filename,
564
480
  oursRef: context.oursRef,
565
481
  theirsRef: context.theirsRef,
566
482
  content: conflictedContent
567
- });
483
+ }, options.language);
568
484
  const result = await generateObject({
569
485
  model,
570
486
  schema: ConflictResolutionSchema,
@@ -574,11 +490,11 @@ async function resolveConflict(conflictedContent, context, options, template) {
574
490
  }
575
491
  async function generateGitignore(context, options, template) {
576
492
  const model = await getModel(options);
577
- const prompt = applyTemplate(template, "gitignore", {
493
+ const prompt = buildPrompt(template, "gitignore", {
578
494
  files: context.files,
579
495
  configFiles: context.configFiles,
580
496
  existingGitignore: context.existingGitignore
581
- });
497
+ }, options.language, "Respond with ONLY the .gitignore content, nothing else. No explanations or markdown code blocks.");
582
498
  const result = await generateText({
583
499
  model,
584
500
  prompt,
@@ -586,6 +502,102 @@ async function generateGitignore(context, options, template) {
586
502
  });
587
503
  return result.text.trim();
588
504
  }
505
+
506
+ // src/lib/config.ts
507
+ import { homedir as homedir2 } from "os";
508
+ import { join as join2 } from "path";
509
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
510
+ import { execSync } from "child_process";
511
+ var DEFAULT_CONFIG = {
512
+ lang: "en"
513
+ };
514
+ function getGlobalConfigPath() {
515
+ const configDir = join2(homedir2(), ".config", "gut");
516
+ return join2(configDir, "config.json");
517
+ }
518
+ function getRepoRoot() {
519
+ try {
520
+ return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
521
+ } catch {
522
+ return null;
523
+ }
524
+ }
525
+ function getLocalConfigPath() {
526
+ const repoRoot = getRepoRoot();
527
+ if (!repoRoot) return null;
528
+ return join2(repoRoot, ".gut", "config.json");
529
+ }
530
+ function ensureGlobalConfigDir() {
531
+ const configDir = join2(homedir2(), ".config", "gut");
532
+ if (!existsSync2(configDir)) {
533
+ mkdirSync(configDir, { recursive: true });
534
+ }
535
+ }
536
+ function ensureLocalConfigDir() {
537
+ const repoRoot = getRepoRoot();
538
+ if (!repoRoot) return;
539
+ const gutDir = join2(repoRoot, ".gut");
540
+ if (!existsSync2(gutDir)) {
541
+ mkdirSync(gutDir, { recursive: true });
542
+ }
543
+ }
544
+ function readConfigFile(path) {
545
+ if (!existsSync2(path)) return {};
546
+ try {
547
+ return JSON.parse(readFileSync2(path, "utf-8"));
548
+ } catch {
549
+ return {};
550
+ }
551
+ }
552
+ function getGlobalConfig() {
553
+ const globalPath = getGlobalConfigPath();
554
+ return { ...DEFAULT_CONFIG, ...readConfigFile(globalPath) };
555
+ }
556
+ function getLocalConfig() {
557
+ const localPath = getLocalConfigPath();
558
+ if (!localPath) return {};
559
+ return readConfigFile(localPath);
560
+ }
561
+ function getConfig() {
562
+ const globalConfig = getGlobalConfig();
563
+ const localConfig = getLocalConfig();
564
+ return { ...globalConfig, ...localConfig };
565
+ }
566
+ function setGlobalConfig(key, value) {
567
+ ensureGlobalConfigDir();
568
+ const config = getGlobalConfig();
569
+ config[key] = value;
570
+ writeFileSync(getGlobalConfigPath(), JSON.stringify(config, null, 2));
571
+ }
572
+ function setLocalConfig(key, value) {
573
+ const localPath = getLocalConfigPath();
574
+ if (!localPath) {
575
+ throw new Error("Not in a git repository");
576
+ }
577
+ ensureLocalConfigDir();
578
+ const config = getLocalConfig();
579
+ config[key] = value;
580
+ writeFileSync(localPath, JSON.stringify(config, null, 2));
581
+ }
582
+ function getLanguage() {
583
+ return getConfig().lang;
584
+ }
585
+ function setLanguage(lang, local = false) {
586
+ if (local) {
587
+ setLocalConfig("lang", lang);
588
+ } else {
589
+ setGlobalConfig("lang", lang);
590
+ }
591
+ }
592
+ function getLanguageInstruction(lang) {
593
+ switch (lang) {
594
+ case "ja":
595
+ return "\n\nIMPORTANT: Respond in Japanese (\u65E5\u672C\u8A9E\u3067\u56DE\u7B54\u3057\u3066\u304F\u3060\u3055\u3044).";
596
+ case "en":
597
+ default:
598
+ return "";
599
+ }
600
+ }
589
601
  export {
590
602
  deleteApiKey,
591
603
  findTemplate,