@riflo/ryte 1.1.3 → 1.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riflo/ryte",
3
- "version": "1.1.3",
3
+ "version": "1.2.2",
4
4
  "description": "AI Git Workflow Assistant - Generate semantic commits and PRs",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/ai.js CHANGED
@@ -1,28 +1,19 @@
1
- import dotenv from "dotenv";
2
- dotenv.config();
1
+ import { getConfig } from "./config.js";
2
+ import { getProviderConfig } from "./provider.js";
3
3
 
4
- export async function generateAIResponse(messages) {
5
- const groqKey = process.env.GROQ_API_KEY;
6
- const openAiKey = process.env.OPENAI_API_KEY;
4
+ export async function generateAIResponse(messages, overrideConfig = null) {
5
+ const config = overrideConfig || getConfig();
7
6
 
8
- if (!groqKey && !openAiKey) {
9
- console.error("Error: Please set either GROQ_API_KEY (for free AI) or OPENAI_API_KEY in your environment variables.");
10
- process.exit(1);
7
+ if (!config || !config.apiKey) {
8
+ throw new Error("API Key not found. Please run 'ryte config' or follow the setup flow.");
11
9
  }
12
10
 
13
- const isGroq = !!groqKey;
14
- const apiKey = isGroq ? groqKey : openAiKey;
11
+ const providerName = config.provider || "openai";
12
+ const pConfig = getProviderConfig(providerName, config.baseUrl);
15
13
 
16
- // Groq API is 100% compatible with OpenAI's format! Just changing URL & Model.
17
- const apiUrl = isGroq
18
- ? "https://api.groq.com/openai/v1/chat/completions"
19
- : "https://api.openai.com/v1/chat/completions";
20
-
21
- // llama-3.1-8b-instant: 14,400 TPM (6x higher than llama-3.3-70b-versatile)
22
- // Still very capable for commit messages and PR summaries
23
- const model = isGroq
24
- ? "llama-3.1-8b-instant"
25
- : "gpt-4o-mini";
14
+ const apiUrl = pConfig.url;
15
+ const apiKey = config.apiKey;
16
+ const model = config.model || pConfig.model;
26
17
 
27
18
  const MAX_RETRIES = 3;
28
19
 
@@ -42,7 +33,6 @@ export async function generateAIResponse(messages) {
42
33
  });
43
34
 
44
35
  if (response.status === 429) {
45
- // Rate limited — parse retry-after header or use exponential backoff
46
36
  const retryAfter = parseInt(response.headers.get("retry-after") || "15", 10);
47
37
  const waitSeconds = retryAfter + 1;
48
38
 
@@ -50,22 +40,67 @@ export async function generateAIResponse(messages) {
50
40
  process.stdout.write(`\r\x1b[33m⚠ Rate limit hit. Waiting ${i}s before retry (${attempt}/${MAX_RETRIES})...\x1b[0m`);
51
41
  await new Promise(r => setTimeout(r, 1000));
52
42
  }
53
- process.stdout.write("\r" + " ".repeat(80) + "\r"); // Clear the line
43
+ process.stdout.write("\r" + " ".repeat(80) + "\r");
54
44
  continue;
55
45
  }
56
46
 
57
47
  if (!response.ok) {
58
- const error = await response.json();
59
- throw new Error(error.error?.message || "API request failed");
48
+ let errorData;
49
+ try {
50
+ errorData = await response.json();
51
+ } catch (e) {
52
+ throw new Error(`HTTP Error ${response.status}: ${response.statusText}`);
53
+ }
54
+ throw new Error(errorData.error?.message || `API request failed with status ${response.status}`);
60
55
  }
61
56
 
62
57
  const data = await response.json();
63
- return data.choices[0].message.content.trim();
58
+ if (!data.choices || !data.choices[0] || !data.choices[0].message) {
59
+ throw new Error("Invalid response format from AI provider.");
60
+ }
61
+
62
+ const content = data.choices[0].message.content;
63
+ if (!content) {
64
+ throw new Error("AI provider returned an empty response.");
65
+ }
66
+
67
+ const trimmed = content.trim();
68
+
69
+ // Deterministic Internal Contract Prep
70
+ // Currently returns { text: string, structured: Object }
71
+ return parseAICommitMessage(trimmed);
64
72
  } catch (e) {
73
+ // Check for network errors (Offline)
74
+ if (e.code === 'ENOTFOUND' || e.code === 'EAI_AGAIN') {
75
+ throw new Error("Network unreachable. Please check your internet connection.");
76
+ }
77
+
65
78
  if (attempt === MAX_RETRIES) {
66
- console.error("AI Generation failed:", e.message);
67
- process.exit(1);
79
+ throw e;
68
80
  }
69
81
  }
70
82
  }
71
83
  }
84
+
85
+ /**
86
+ * Parses a conventional commit message into a structured object.
87
+ * This is the 'Deterministic Contract' for the engine.
88
+ */
89
+ function parseAICommitMessage(text) {
90
+ const lines = text.split("\n");
91
+ const header = lines[0].trim();
92
+
93
+ // Simple regex for conventional commit: type(scope): subject
94
+ const match = header.match(/^(\w+)(?:\(([^)]+)\))?:\s*(.+)$/);
95
+
96
+ return {
97
+ text: text, // The full raw text for legacy compatibility
98
+ structured: {
99
+ header: header,
100
+ type: match ? match[1] : "other",
101
+ scope: match ? match[2] : null,
102
+ subject: match ? match[3] : header,
103
+ body: lines.slice(1).filter(l => l.trim().length > 0).join("\n").trim() || null
104
+ }
105
+ };
106
+ }
package/src/config.js ADDED
@@ -0,0 +1,45 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), ".ryte");
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
7
+
8
+ const DEFAULT_CONFIG = {
9
+ provider: "openai",
10
+ apiKey: "",
11
+ model: "gpt-4o-mini",
12
+ baseUrl: "" // Optional for local providers like OpenClaw/Ollama
13
+ };
14
+
15
+ export function getConfig() {
16
+ if (!fs.existsSync(CONFIG_FILE)) {
17
+ return null;
18
+ }
19
+ try {
20
+ const data = fs.readFileSync(CONFIG_FILE, "utf-8");
21
+ const parsed = JSON.parse(data);
22
+ // Ensure some basic structure integrity
23
+ if (typeof parsed !== "object" || parsed === null) throw new Error("Invalid config format");
24
+ return { ...DEFAULT_CONFIG, ...parsed };
25
+ } catch (e) {
26
+ console.warn(`\n\x1b[33m⚠ Warning: Configuration file is corrupted. Re-initializing...\x1b[0m`);
27
+ // If corrupted, return null so setupFlow triggers or we can fallback
28
+ return null;
29
+ }
30
+ }
31
+
32
+ export function setConfig(updates) {
33
+ if (!fs.existsSync(CONFIG_DIR)) {
34
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
35
+ }
36
+ const current = getConfig() || DEFAULT_CONFIG;
37
+ const updated = { ...current, ...updates };
38
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2), { mode: 0o600 });
39
+ return updated;
40
+ }
41
+
42
+ export function hasValidConfig() {
43
+ const config = getConfig();
44
+ return !!(config && config.apiKey);
45
+ }
package/src/index.js CHANGED
@@ -6,12 +6,16 @@ import path from "path";
6
6
  import { getStagedDiff, getCurrentBranch, getBranchCommits, applyCommit } from "./git.js";
7
7
  import { generateAIResponse } from "./ai.js";
8
8
  import { COMMIT_SYSTEM_PROMPT, PR_SYSTEM_PROMPT } from "./prompt.js";
9
+ import { getConfig, setConfig, hasValidConfig } from "./config.js";
10
+ import { PROVIDERS } from "./provider.js";
9
11
 
10
12
  const rl = readline.createInterface({
11
13
  input: process.stdin,
12
14
  output: process.stdout
13
15
  });
14
16
 
17
+ const VERSION = "1.2.2";
18
+
15
19
  async function question(query) {
16
20
  return new Promise(resolve => rl.question(query, resolve));
17
21
  }
@@ -40,6 +44,43 @@ function editInteractively(initialText) {
40
44
  });
41
45
  }
42
46
 
47
+ async function setupFlow() {
48
+ console.log("\n\x1b[36mWelcome to RYTE. Let's set up your Git Intelligence Layer.\x1b[0m");
49
+ console.log("------------------------------------------------------------");
50
+
51
+ console.log("\nSelect your LLM Provider:");
52
+ const providerList = Object.keys(PROVIDERS);
53
+ providerList.forEach((p, i) => console.log(`${i + 1}) ${p.charAt(0).toUpperCase() + p.slice(1)}`));
54
+
55
+ const choice = await question(`\nChoose [1-${providerList.length}]: `);
56
+ const providerKey = providerList[parseInt(choice) - 1] || "openai";
57
+
58
+ const apiKey = await question(`Paste your ${providerKey.toUpperCase()} API Key: `);
59
+ if (!apiKey) {
60
+ console.error("Error: API Key is required.");
61
+ process.exit(1);
62
+ }
63
+
64
+ setConfig({
65
+ provider: providerKey,
66
+ apiKey: apiKey,
67
+ model: PROVIDERS[providerKey].defaultModel
68
+ });
69
+
70
+ console.log("\n\x1b[32m✔ Configuration saved to ~/.ryte/config.json\x1b[0m");
71
+ }
72
+
73
+ async function handleConfig() {
74
+ const config = getConfig() || {};
75
+ console.log("\n\x1b[36mCurrent Configuration:\x1b[0m");
76
+ console.log(JSON.stringify(config, null, 2));
77
+
78
+ const choice = await question("\nWould you like to reset configuration? [y/N]: ");
79
+ if (choice.toLowerCase() === "y") {
80
+ await setupFlow();
81
+ }
82
+ }
83
+
43
84
  async function interactiveLoop(initialResult, type) {
44
85
  let currentResult = initialResult;
45
86
 
@@ -72,25 +113,34 @@ async function interactiveLoop(initialResult, type) {
72
113
  async function handleCommit() {
73
114
  const diff = getStagedDiff();
74
115
  if (!diff) {
75
- console.log("No staged changes found. Use `git add` to stage files.");
116
+ console.log("\n\x1b[33m⚠ No staged changes found.\x1b[0m");
117
+ console.log("Use \x1b[32m`git add` \x1b[0m to stage files before committing.");
76
118
  process.exit(0);
77
119
  }
78
120
 
79
121
  const branch = getCurrentBranch();
80
122
 
81
123
  while (true) {
82
- console.log("\nAnalyzing staged diff...");
83
- const result = await generateAIResponse([
84
- { role: "system", content: COMMIT_SYSTEM_PROMPT },
85
- { role: "user", content: `Branch: ${branch}\n\nDiff:\n${diff}` }
86
- ]);
87
-
88
- const finalAction = await interactiveLoop(result, "Commit Message");
89
-
90
- if (finalAction !== "REGENERATE") {
91
- applyCommit(finalAction);
92
- console.log("\n\x1b[32m✔ Commit applied successfully!\x1b[0m");
93
- break;
124
+ try {
125
+ console.log("\nAnalyzing staged diff...");
126
+ const result = await generateAIResponse([
127
+ { role: "system", content: COMMIT_SYSTEM_PROMPT },
128
+ { role: "user", content: `Branch: ${branch}\n\nDiff:\n${diff}` }
129
+ ]);
130
+
131
+ const finalAction = await interactiveLoop(result.text, "Commit Message");
132
+
133
+ if (finalAction !== "REGENERATE") {
134
+ applyCommit(finalAction);
135
+ console.log("\n\x1b[32m✔ Commit applied successfully!\x1b[0m");
136
+ break;
137
+ }
138
+ } catch (e) {
139
+ console.error(`\n\x1b[31m✖ AI Generation failed:\x1b[0m ${e.message}`);
140
+ const retry = await question("\nWould you like to try again? [y/N]: ");
141
+ if (retry.toLowerCase() !== "y") {
142
+ process.exit(1);
143
+ }
94
144
  }
95
145
  }
96
146
  }
@@ -98,25 +148,34 @@ async function handleCommit() {
98
148
  async function handlePR() {
99
149
  const commits = getBranchCommits();
100
150
  if (!commits) {
101
- console.log("No recent commits found distinct from main branch.");
151
+ console.log("\n\x1b[33m⚠ No recent commits found.\x1b[0m");
152
+ console.log("Ensure you have committed changes that are distinct from your main branch.");
102
153
  process.exit(0);
103
154
  }
104
155
  const branch = getCurrentBranch();
105
156
 
106
157
  while (true) {
107
- console.log("\nAnalyzing recent commits...");
108
- const result = await generateAIResponse([
109
- { role: "system", content: PR_SYSTEM_PROMPT },
110
- { role: "user", content: `Branch: ${branch}\n\nCommits:\n${commits}` }
111
- ]);
112
-
113
- const finalAction = await interactiveLoop(result, "PR Markdown Description");
114
-
115
- if (finalAction !== "REGENERATE") {
116
- console.log("\n\x1b[32mFinal PR Content:\x1b[0m\n");
117
- console.log(finalAction);
118
- console.log("\n(You can copy-paste the above into your pull request or we can pipe it to the clipboard later)");
119
- break;
158
+ try {
159
+ console.log("\nAnalyzing recent commits...");
160
+ const result = await generateAIResponse([
161
+ { role: "system", content: PR_SYSTEM_PROMPT },
162
+ { role: "user", content: `Branch: ${branch}\n\nCommits:\n${commits}` }
163
+ ]);
164
+
165
+ const finalAction = await interactiveLoop(result.text, "PR Markdown Description");
166
+
167
+ if (finalAction !== "REGENERATE") {
168
+ console.log("\n\x1b[32mFinal PR Content:\x1b[0m\n");
169
+ console.log(finalAction);
170
+ console.log("\n(You can copy-paste the above into your pull request)");
171
+ break;
172
+ }
173
+ } catch (e) {
174
+ console.error(`\n\x1b[31m✖ AI Generation failed:\x1b[0m ${e.message}`);
175
+ const retry = await question("\nWould you like to try again? [y/N]: ");
176
+ if (retry.toLowerCase() !== "y") {
177
+ process.exit(1);
178
+ }
120
179
  }
121
180
  }
122
181
  }
@@ -125,12 +184,19 @@ async function main() {
125
184
  const args = process.argv.slice(2);
126
185
  const cmd = args[0]?.toLowerCase();
127
186
 
128
- if (cmd === "c" || cmd === "commit") {
129
- await handleCommit();
130
- } else if (cmd === "pr") {
131
- await handlePR();
132
- } else {
133
- console.log(`
187
+ try {
188
+ if (!hasValidConfig()) {
189
+ await setupFlow();
190
+ }
191
+
192
+ if (cmd === "c" || cmd === "commit") {
193
+ await handleCommit();
194
+ } else if (cmd === "pr") {
195
+ await handlePR();
196
+ } else if (cmd === "config") {
197
+ await handleConfig();
198
+ } else {
199
+ console.log(`
134
200
  \x1b[1;38;5;39m██████╗ \x1b[1;38;5;63m██╗ ██╗\x1b[1;38;5;129m████████╗\x1b[1;38;5;161m███████╗\x1b[0m
135
201
  \x1b[1;38;5;39m██╔══██╗\x1b[1;38;5;63m╚██╗ ██╔╝\x1b[1;38;5;129m╚══██╔══╝\x1b[1;38;5;161m██╔════╝\x1b[0m
136
202
  \x1b[1;38;5;39m██████╔╝\x1b[1;38;5;63m ╚████╔╝ \x1b[1;38;5;129m ██║ \x1b[1;38;5;161m█████╗ \x1b[0m
@@ -139,17 +205,21 @@ async function main() {
139
205
  \x1b[1;38;5;39m╚═╝ ╚═╝\x1b[1;38;5;63m ╚═╝ \x1b[1;38;5;129m ╚═╝ \x1b[1;38;5;161m╚══════╝\x1b[0m
140
206
 
141
207
  \x1b[1;38;5;46m[ THE AI-POWERED GIT INFRASTRUCTURE ]\x1b[0m
142
- \x1b[90mv1.1.3 | by Riflo\x1b[0m
208
+ \x1b[90mv${VERSION} | by Riflo\x1b[0m
143
209
 
144
210
  \x1b[33mCOMMANDS:\x1b[0m
145
- \x1b[32mryte c\x1b[0m Generate semantic commit from diff
146
- \x1b[32mryte pr\x1b[0m Generate PR markdown from branch commits
147
-
148
- \x1b[33mGETTING STARTED:\x1b[0m
149
- Set either environment variable to unleash the AI:
150
- \x1b[36mGROQ_API_KEY\x1b[0m (Recommended / Free tier)
151
- \x1b[36mOPENAI_API_KEY\x1b[0m (OpenAI API key)
152
- `);
211
+ \x1b[32mryte c\x1b[0m Generate semantic commit from diff
212
+ \x1b[32mryte pr\x1b[0m Generate PR markdown from branch commits
213
+ \x1b[32mryte config\x1b[0m Generate or edit your local configuration
214
+
215
+ \x1b[33mONBOARDING:\x1b[0m
216
+ No .env required. Run \x1b[32mryte config\x1b[0m or just run \x1b[32mryte c\x1b[0m to
217
+ start the interactive setup.
218
+ `);
219
+ }
220
+ } catch (e) {
221
+ console.error(`\n\x1b[31m✖ Unexpected Error:\x1b[0m ${e.message}`);
222
+ process.exit(1);
153
223
  }
154
224
 
155
225
  rl.close();
package/src/prompt.js CHANGED
@@ -2,25 +2,33 @@ export const COMMIT_SYSTEM_PROMPT = `
2
2
  You are an expert developer assistant specialized in Git workflows and the Conventional Commits specification.
3
3
  Your goal is to generate a concise, meaningful, and technically accurate commit message based on provided git diffs.
4
4
 
5
+ PRIORITY:
6
+ 1. LOGIC OVER METADATA: If the diff contains both code changes (src/, lib/) and metadata changes (package.json version, README typos), the commit message MUST focus on the code logic.
7
+ 2. SUBSTANCE: Avoid subjects like "bump version" or "update files" if there is meaningful logic change. Focus on the "What" and "Why" of the code evolution.
8
+
5
9
  RULES:
6
- 1. OUTPUT ONLY THE COMMIT MESSAGE. No markdown, no "Here is your commit", no backticks.
10
+ 1. OUTPUT ONLY THE COMMIT MESSAGE. No markdown, no filler.
7
11
  2. Follow Conventional Commits: <type>(<scope>): <subject>
8
- 3. Use types: feat (new feature), fix (bug fix), docs (documentation), style (formatting), refactor (code cleanup), perf (performance), test (adding tests), chore (maintenance).
12
+ 3. Use types:
13
+ - feat: new capability or significant hardening/stabilization.
14
+ - fix: bug fixes.
15
+ - refactor: code restructuring without changing behavior.
16
+ - docs: documentation only.
17
+ - chore: maintenance, version bumps (only if NO logic changed).
9
18
  4. Subject line:
10
- - Use imperative mood ("add", not "adds" or "added").
11
- - Max 50 characters.
12
- - Do not end with a period.
13
- - Focus on the "why" or the core "what", not every trivial change.
14
- 5. Breaking Changes: If the diff shows breaking changes, use "!" after the type (e.g., "feat!: delete deprecated api").
15
- 6. Scope: If the diff is localized, infer a scope (e.g., "auth", "ui", "config").
16
- 7. Body (Optional): If the change is complex, add a brief body after 1 blank line to explain technical nuances.
17
- 8. Context: If a branch name or ticket is provided, incorporate it into the scope or footer if applicable.
18
- 9. Anti-Hallucination: Documentation files (like README.md) often contain example commit messages (e.g., "feat(auth): ..."). DO NOT assume these examples are the topic of the current change.
19
- 10. Logical Validation: Your suggested <scope> must be derived from actual modified logic in the diff, not from text inside code blocks, comments, or examples within a documentation file.
20
- 11. If only README.md or docs are changed, the type MUST be "docs" and the scope should relate to the documentation structure (e.g., "readme", "config", "intro"), NOT the example code inside it.
19
+ - Use imperative mood ("add", "implement", "harden").
20
+ - Max 50 characters. No period.
21
+ - Focus on the technical achievement.
22
+ 5. Breaking Changes: Use "!" if behavior changes significantly (e.g., "feat!: ...").
23
+ 6. Scope: Infer from affected module (e.g., "config", "ai", "core").
24
+ 7. Body: Use bullet points for complex multi-file changes to explain "Why" and nuances.
21
25
 
22
26
  Example:
23
- feat(ui): add loading state to checkout button
27
+ feat(core): harden config recovery and ai response validation
28
+
29
+ - Implement auto-healing for corrupted config.json
30
+ - Add defensive network error handling in ai.js
31
+ - Standardize internal engine response contract
24
32
  `;
25
33
 
26
34
  export const PR_SYSTEM_PROMPT = `
@@ -0,0 +1,31 @@
1
+ export const PROVIDERS = {
2
+ openai: {
3
+ baseUrl: "https://api.openai.com/v1/chat/completions",
4
+ defaultModel: "gpt-4o-mini",
5
+ authHeader: (key) => `Bearer ${key}`
6
+ },
7
+ groq: {
8
+ baseUrl: "https://api.groq.com/openai/v1/chat/completions",
9
+ defaultModel: "llama-3.1-8b-instant",
10
+ authHeader: (key) => `Bearer ${key}`
11
+ },
12
+ openrouter: {
13
+ baseUrl: "https://openrouter.ai/api/v1/chat/completions",
14
+ defaultModel: "google/gemini-pro-1.5-exp",
15
+ authHeader: (key) => `Bearer ${key}`
16
+ },
17
+ local: {
18
+ baseUrl: "http://localhost:11434/v1/chat/completions", // Default Ollama/OpenClaw local port
19
+ defaultModel: "llama3",
20
+ authHeader: (key) => `Bearer ${key}`
21
+ }
22
+ };
23
+
24
+ export function getProviderConfig(name, customBaseUrl = "") {
25
+ const p = PROVIDERS[name] || PROVIDERS.openai;
26
+ return {
27
+ url: customBaseUrl || p.baseUrl,
28
+ model: p.defaultModel,
29
+ auth: p.authHeader
30
+ };
31
+ }