gut-cli 0.1.18 → 0.1.20

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,8 +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";
8
+ import { existsSync, readFileSync } from "fs";
9
+ import { join, dirname } from "path";
10
+ import { homedir } from "os";
10
11
  import { fileURLToPath } from "url";
11
12
 
12
13
  // src/lib/credentials.ts
@@ -79,144 +80,63 @@ async function listProviders() {
79
80
  return results;
80
81
  }
81
82
 
82
- // src/lib/config.ts
83
- import { homedir } from "os";
84
- import { join } from "path";
85
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
86
- import { execSync } from "child_process";
87
- var DEFAULT_CONFIG = {
88
- lang: "en"
89
- };
90
- function getGlobalConfigPath() {
91
- const configDir = join(homedir(), ".config", "gut");
92
- return join(configDir, "config.json");
93
- }
94
- function getRepoRoot() {
95
- try {
96
- return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
97
- } catch {
98
- return null;
99
- }
100
- }
101
- function getLocalConfigPath() {
102
- const repoRoot = getRepoRoot();
103
- if (!repoRoot) return null;
104
- return join(repoRoot, ".gut", "config.json");
105
- }
106
- function ensureGlobalConfigDir() {
107
- const configDir = join(homedir(), ".config", "gut");
108
- if (!existsSync(configDir)) {
109
- mkdirSync(configDir, { recursive: true });
110
- }
111
- }
112
- function ensureLocalConfigDir() {
113
- const repoRoot = getRepoRoot();
114
- if (!repoRoot) return;
115
- const gutDir = join(repoRoot, ".gut");
116
- if (!existsSync(gutDir)) {
117
- mkdirSync(gutDir, { recursive: true });
118
- }
119
- }
120
- function readConfigFile(path) {
121
- if (!existsSync(path)) return {};
122
- try {
123
- return JSON.parse(readFileSync(path, "utf-8"));
124
- } catch {
125
- return {};
126
- }
127
- }
128
- function getGlobalConfig() {
129
- const globalPath = getGlobalConfigPath();
130
- return { ...DEFAULT_CONFIG, ...readConfigFile(globalPath) };
131
- }
132
- function getLocalConfig() {
133
- const localPath = getLocalConfigPath();
134
- if (!localPath) return {};
135
- return readConfigFile(localPath);
136
- }
137
- function getConfig() {
138
- const globalConfig = getGlobalConfig();
139
- const localConfig = getLocalConfig();
140
- return { ...globalConfig, ...localConfig };
141
- }
142
- function setGlobalConfig(key, value) {
143
- ensureGlobalConfigDir();
144
- const config = getGlobalConfig();
145
- config[key] = value;
146
- writeFileSync(getGlobalConfigPath(), JSON.stringify(config, null, 2));
147
- }
148
- function setLocalConfig(key, value) {
149
- const localPath = getLocalConfigPath();
150
- if (!localPath) {
151
- throw new Error("Not in a git repository");
152
- }
153
- ensureLocalConfigDir();
154
- const config = getLocalConfig();
155
- config[key] = value;
156
- writeFileSync(localPath, JSON.stringify(config, null, 2));
157
- }
158
- function getLanguage() {
159
- return getConfig().lang;
160
- }
161
- function setLanguage(lang, local = false) {
162
- if (local) {
163
- setLocalConfig("lang", lang);
164
- } else {
165
- setGlobalConfig("lang", lang);
166
- }
167
- }
168
- function getLanguageInstruction(lang) {
169
- switch (lang) {
170
- case "ja":
171
- return "\n\nIMPORTANT: Respond in Japanese (\u65E5\u672C\u8A9E\u3067\u56DE\u7B54\u3057\u3066\u304F\u3060\u3055\u3044).";
172
- case "en":
173
- default:
174
- return "";
175
- }
176
- }
177
-
178
83
  // src/lib/ai.ts
179
84
  var __filename = fileURLToPath(import.meta.url);
180
85
  var __dirname = dirname(__filename);
181
86
  function findGutRoot() {
182
87
  let current = __dirname;
183
88
  for (let i = 0; i < 5; i++) {
184
- const gutPath = join2(current, ".gut");
185
- if (existsSync2(gutPath)) {
89
+ const gutPath = join(current, ".gut");
90
+ if (existsSync(gutPath)) {
186
91
  return current;
187
92
  }
188
93
  current = dirname(current);
189
94
  }
190
- return join2(__dirname, "..");
95
+ return join(__dirname, "..");
191
96
  }
192
97
  var GUT_ROOT = findGutRoot();
193
98
  function loadTemplate(name) {
194
- const templatePath = join2(GUT_ROOT, ".gut", `${name}.md`);
195
- if (existsSync2(templatePath)) {
196
- return readFileSync2(templatePath, "utf-8");
99
+ const templatePath = join(GUT_ROOT, ".gut", `${name}.md`);
100
+ if (existsSync(templatePath)) {
101
+ return readFileSync(templatePath, "utf-8");
197
102
  }
198
103
  throw new Error(`Template not found: ${templatePath}`);
199
104
  }
200
- function findTemplate(repoRoot, templateName) {
201
- const templatePath = join2(repoRoot, ".gut", `${templateName}.md`);
202
- if (existsSync2(templatePath)) {
203
- return readFileSync2(templatePath, "utf-8");
105
+ function getGlobalTemplatesDir() {
106
+ return join(homedir(), ".config", "gut", "templates");
107
+ }
108
+ function findGlobalTemplate(templateName) {
109
+ const templatePath = join(getGlobalTemplatesDir(), `${templateName}.md`);
110
+ if (existsSync(templatePath)) {
111
+ return readFileSync(templatePath, "utf-8");
204
112
  }
205
113
  return null;
206
114
  }
207
- function applyTemplate(userTemplate, templateName, variables) {
208
- const langInstruction = getLanguageInstruction(getLanguage());
209
- let result = userTemplate || loadTemplate(templateName);
210
- result = result.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, content) => {
211
- return variables[key] ? content : "";
212
- });
213
- for (const [key, value] of Object.entries(variables)) {
214
- result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value || "");
115
+ function findTemplate(repoRoot, templateName) {
116
+ const projectTemplatePath = join(repoRoot, ".gut", `${templateName}.md`);
117
+ if (existsSync(projectTemplatePath)) {
118
+ return readFileSync(projectTemplatePath, "utf-8");
215
119
  }
216
- if (langInstruction) {
217
- result += langInstruction;
120
+ const globalTemplate = findGlobalTemplate(templateName);
121
+ if (globalTemplate) {
122
+ return globalTemplate;
218
123
  }
219
- return result;
124
+ return null;
125
+ }
126
+ function buildPrompt(userTemplate, templateName, context, language) {
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
+ }
135
+ }
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
+ return contextXml + "<instructions>\n" + template + langInstruction + "\n</instructions>";
220
140
  }
221
141
  var DEFAULT_MODELS = {
222
142
  gemini: "gemini-2.0-flash",
@@ -264,9 +184,9 @@ async function getModel(options) {
264
184
  }
265
185
  async function generateCommitMessage(diff, options, template) {
266
186
  const model = await getModel(options);
267
- const prompt = applyTemplate(template, "commit", {
187
+ const prompt = buildPrompt(template, "commit", {
268
188
  diff: diff.slice(0, 8e3)
269
- });
189
+ }, options.language);
270
190
  const result = await generateText({
271
191
  model,
272
192
  prompt,
@@ -276,12 +196,12 @@ async function generateCommitMessage(diff, options, template) {
276
196
  }
277
197
  async function generatePRDescription(context, options, template) {
278
198
  const model = await getModel(options);
279
- const prompt = applyTemplate(template, "pr", {
199
+ const prompt = buildPrompt(template, "pr", {
280
200
  baseBranch: context.baseBranch,
281
201
  currentBranch: context.currentBranch,
282
202
  commits: context.commits.map((c) => `- ${c}`).join("\n"),
283
203
  diff: context.diff.slice(0, 6e3)
284
- });
204
+ }, options.language);
285
205
  const result = await generateText({
286
206
  model,
287
207
  prompt,
@@ -312,9 +232,9 @@ var CodeReviewSchema = z.object({
312
232
  });
313
233
  async function generateCodeReview(diff, options, template) {
314
234
  const model = await getModel(options);
315
- const prompt = applyTemplate(template, "review", {
235
+ const prompt = buildPrompt(template, "review", {
316
236
  diff: diff.slice(0, 1e4)
317
- });
237
+ }, options.language);
318
238
  const result = await generateObject({
319
239
  model,
320
240
  schema: CodeReviewSchema,
@@ -336,13 +256,13 @@ var ChangelogSchema = z.object({
336
256
  async function generateChangelog(context, options, template) {
337
257
  const model = await getModel(options);
338
258
  const commitList = context.commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message} (${c.author})`).join("\n");
339
- const prompt = applyTemplate(template, "changelog", {
259
+ const prompt = buildPrompt(template, "changelog", {
340
260
  fromRef: context.fromRef,
341
261
  toRef: context.toRef,
342
262
  commits: commitList,
343
263
  diff: context.diff.slice(0, 8e3),
344
264
  todayDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
345
- });
265
+ }, options.language);
346
266
  const result = await generateObject({
347
267
  model,
348
268
  schema: ChangelogSchema,
@@ -370,10 +290,10 @@ var ExplanationSchema = z.object({
370
290
  async function generateExplanation(context, options, template) {
371
291
  const model = await getModel(options);
372
292
  if (context.type === "file-content") {
373
- const prompt2 = applyTemplate(template, "explain-file", {
293
+ const prompt2 = buildPrompt(template, "explain-file", {
374
294
  filePath: context.metadata.filePath || "",
375
295
  content: context.content?.slice(0, 15e3) || ""
376
- });
296
+ }, options.language);
377
297
  const result2 = await generateObject({
378
298
  model,
379
299
  schema: ExplanationSchema,
@@ -407,11 +327,11 @@ Author: ${context.metadata.author}
407
327
  Date: ${context.metadata.date}`;
408
328
  targetType = "commit";
409
329
  }
410
- const prompt = applyTemplate(template, "explain", {
330
+ const prompt = buildPrompt(template, "explain", {
411
331
  targetType,
412
- context: contextInfo,
332
+ contextInfo,
413
333
  diff: context.diff?.slice(0, 12e3) || ""
414
- });
334
+ }, options.language);
415
335
  const result = await generateObject({
416
336
  model,
417
337
  schema: ExplanationSchema,
@@ -431,11 +351,11 @@ var CommitSearchSchema = z.object({
431
351
  async function searchCommits(query, commits, options, maxResults = 5, template) {
432
352
  const model = await getModel(options);
433
353
  const commitList = commits.map((c) => `${c.hash.slice(0, 7)} | ${c.author} | ${c.date.split("T")[0]} | ${c.message.split("\n")[0]}`).join("\n");
434
- const prompt = applyTemplate(template, "find", {
354
+ const prompt = buildPrompt(template, "find", {
435
355
  query,
436
356
  commits: commitList,
437
357
  maxResults: String(maxResults)
438
- });
358
+ }, options.language);
439
359
  const result = await generateObject({
440
360
  model,
441
361
  schema: CommitSearchSchema,
@@ -464,11 +384,11 @@ async function searchCommits(query, commits, options, maxResults = 5, template)
464
384
  }
465
385
  async function generateBranchName(description, options, context, template) {
466
386
  const model = await getModel(options);
467
- const prompt = applyTemplate(template, "branch", {
387
+ const prompt = buildPrompt(template, "branch", {
468
388
  description,
469
389
  type: context?.type,
470
390
  issue: context?.issue
471
- });
391
+ }, options.language);
472
392
  const result = await generateText({
473
393
  model,
474
394
  prompt,
@@ -478,9 +398,9 @@ async function generateBranchName(description, options, context, template) {
478
398
  }
479
399
  async function generateBranchNameFromDiff(diff, options, template) {
480
400
  const model = await getModel(options);
481
- const prompt = applyTemplate(template, "checkout", {
401
+ const prompt = buildPrompt(template, "checkout", {
482
402
  diff: diff.slice(0, 8e3)
483
- });
403
+ }, options.language);
484
404
  const result = await generateText({
485
405
  model,
486
406
  prompt,
@@ -490,9 +410,9 @@ async function generateBranchNameFromDiff(diff, options, template) {
490
410
  }
491
411
  async function generateStashName(diff, options, template) {
492
412
  const model = await getModel(options);
493
- const prompt = applyTemplate(template, "stash", {
413
+ const prompt = buildPrompt(template, "stash", {
494
414
  diff: diff.slice(0, 4e3)
495
- });
415
+ }, options.language);
496
416
  const result = await generateText({
497
417
  model,
498
418
  prompt,
@@ -522,13 +442,13 @@ async function generateWorkSummary(context, options, format = "custom", template
522
442
  const commitList = context.commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message.split("\n")[0]} (${c.date.split("T")[0]})`).join("\n");
523
443
  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}` : ""}.`;
524
444
  const period = `${context.since}${context.until ? ` to ${context.until}` : " to now"}`;
525
- const prompt = applyTemplate(template, "summary", {
445
+ const prompt = buildPrompt(template, "summary", {
526
446
  author: context.author,
527
447
  period,
528
448
  format: formatHint,
529
449
  commits: commitList,
530
450
  diff: context.diff?.slice(0, 6e3)
531
- });
451
+ }, options.language);
532
452
  const result = await generateObject({
533
453
  model,
534
454
  schema: WorkSummarySchema,
@@ -544,12 +464,12 @@ async function generateWorkSummary(context, options, format = "custom", template
544
464
  }
545
465
  async function resolveConflict(conflictedContent, context, options, template) {
546
466
  const model = await getModel(options);
547
- const prompt = applyTemplate(template, "merge", {
467
+ const prompt = buildPrompt(template, "merge", {
548
468
  filename: context.filename,
549
469
  oursRef: context.oursRef,
550
470
  theirsRef: context.theirsRef,
551
471
  content: conflictedContent
552
- });
472
+ }, options.language);
553
473
  const result = await generateObject({
554
474
  model,
555
475
  schema: ConflictResolutionSchema,
@@ -559,11 +479,11 @@ async function resolveConflict(conflictedContent, context, options, template) {
559
479
  }
560
480
  async function generateGitignore(context, options, template) {
561
481
  const model = await getModel(options);
562
- const prompt = applyTemplate(template, "gitignore", {
482
+ const prompt = buildPrompt(template, "gitignore", {
563
483
  files: context.files,
564
484
  configFiles: context.configFiles,
565
485
  existingGitignore: context.existingGitignore
566
- });
486
+ }, options.language);
567
487
  const result = await generateText({
568
488
  model,
569
489
  prompt,
@@ -571,6 +491,102 @@ async function generateGitignore(context, options, template) {
571
491
  });
572
492
  return result.text.trim();
573
493
  }
494
+
495
+ // src/lib/config.ts
496
+ import { homedir as homedir2 } from "os";
497
+ import { join as join2 } from "path";
498
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
499
+ import { execSync } from "child_process";
500
+ var DEFAULT_CONFIG = {
501
+ lang: "en"
502
+ };
503
+ function getGlobalConfigPath() {
504
+ const configDir = join2(homedir2(), ".config", "gut");
505
+ return join2(configDir, "config.json");
506
+ }
507
+ function getRepoRoot() {
508
+ try {
509
+ return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
510
+ } catch {
511
+ return null;
512
+ }
513
+ }
514
+ function getLocalConfigPath() {
515
+ const repoRoot = getRepoRoot();
516
+ if (!repoRoot) return null;
517
+ return join2(repoRoot, ".gut", "config.json");
518
+ }
519
+ function ensureGlobalConfigDir() {
520
+ const configDir = join2(homedir2(), ".config", "gut");
521
+ if (!existsSync2(configDir)) {
522
+ mkdirSync(configDir, { recursive: true });
523
+ }
524
+ }
525
+ function ensureLocalConfigDir() {
526
+ const repoRoot = getRepoRoot();
527
+ if (!repoRoot) return;
528
+ const gutDir = join2(repoRoot, ".gut");
529
+ if (!existsSync2(gutDir)) {
530
+ mkdirSync(gutDir, { recursive: true });
531
+ }
532
+ }
533
+ function readConfigFile(path) {
534
+ if (!existsSync2(path)) return {};
535
+ try {
536
+ return JSON.parse(readFileSync2(path, "utf-8"));
537
+ } catch {
538
+ return {};
539
+ }
540
+ }
541
+ function getGlobalConfig() {
542
+ const globalPath = getGlobalConfigPath();
543
+ return { ...DEFAULT_CONFIG, ...readConfigFile(globalPath) };
544
+ }
545
+ function getLocalConfig() {
546
+ const localPath = getLocalConfigPath();
547
+ if (!localPath) return {};
548
+ return readConfigFile(localPath);
549
+ }
550
+ function getConfig() {
551
+ const globalConfig = getGlobalConfig();
552
+ const localConfig = getLocalConfig();
553
+ return { ...globalConfig, ...localConfig };
554
+ }
555
+ function setGlobalConfig(key, value) {
556
+ ensureGlobalConfigDir();
557
+ const config = getGlobalConfig();
558
+ config[key] = value;
559
+ writeFileSync(getGlobalConfigPath(), JSON.stringify(config, null, 2));
560
+ }
561
+ function setLocalConfig(key, value) {
562
+ const localPath = getLocalConfigPath();
563
+ if (!localPath) {
564
+ throw new Error("Not in a git repository");
565
+ }
566
+ ensureLocalConfigDir();
567
+ const config = getLocalConfig();
568
+ config[key] = value;
569
+ writeFileSync(localPath, JSON.stringify(config, null, 2));
570
+ }
571
+ function getLanguage() {
572
+ return getConfig().lang;
573
+ }
574
+ function setLanguage(lang, local = false) {
575
+ if (local) {
576
+ setLocalConfig("lang", lang);
577
+ } else {
578
+ setGlobalConfig("lang", lang);
579
+ }
580
+ }
581
+ function getLanguageInstruction(lang) {
582
+ switch (lang) {
583
+ case "ja":
584
+ return "\n\nIMPORTANT: Respond in Japanese (\u65E5\u672C\u8A9E\u3067\u56DE\u7B54\u3057\u3066\u304F\u3060\u3055\u3044).";
585
+ case "en":
586
+ default:
587
+ return "";
588
+ }
589
+ }
574
590
  export {
575
591
  deleteApiKey,
576
592
  findTemplate,