krims-code-cli 1.5.2__py3-none-any.whl

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 (43) hide show
  1. krims_code_cli-1.5.2.dist-info/METADATA +336 -0
  2. krims_code_cli-1.5.2.dist-info/RECORD +43 -0
  3. krims_code_cli-1.5.2.dist-info/WHEEL +5 -0
  4. krims_code_cli-1.5.2.dist-info/entry_points.txt +2 -0
  5. krims_code_cli-1.5.2.dist-info/licenses/LICENSE +21 -0
  6. krims_code_cli-1.5.2.dist-info/top_level.txt +1 -0
  7. krims_pip/__init__.py +1 -0
  8. krims_pip/cli.py +50 -0
  9. krims_pip/node_project/bin/krims-code.js +10 -0
  10. krims_pip/node_project/package-lock.json +793 -0
  11. krims_pip/node_project/package.json +46 -0
  12. krims_pip/node_project/src/agent.js +415 -0
  13. krims_pip/node_project/src/ai/fallback.js +239 -0
  14. krims_pip/node_project/src/ai/google.js +87 -0
  15. krims_pip/node_project/src/ai/providers.js +203 -0
  16. krims_pip/node_project/src/ai/router.js +146 -0
  17. krims_pip/node_project/src/ai/search.js +94 -0
  18. krims_pip/node_project/src/ai/telemetry.js +125 -0
  19. krims_pip/node_project/src/ai/tokens.js +101 -0
  20. krims_pip/node_project/src/ai/universal.js +507 -0
  21. krims_pip/node_project/src/ai/xai.js +50 -0
  22. krims_pip/node_project/src/chat.js +2543 -0
  23. krims_pip/node_project/src/cli.js +632 -0
  24. krims_pip/node_project/src/commands/commit.js +157 -0
  25. krims_pip/node_project/src/commands/dashboard.js +59 -0
  26. krims_pip/node_project/src/commands/github.js +349 -0
  27. krims_pip/node_project/src/commands/index.js +67 -0
  28. krims_pip/node_project/src/commands/status.js +66 -0
  29. krims_pip/node_project/src/commands/teamwork-preview.js +326 -0
  30. krims_pip/node_project/src/commands/theme.js +42 -0
  31. krims_pip/node_project/src/commands/themes.js +30 -0
  32. krims_pip/node_project/src/config.js +400 -0
  33. krims_pip/node_project/src/dashboard.js +112 -0
  34. krims_pip/node_project/src/file-parser.js +164 -0
  35. krims_pip/node_project/src/git.js +41 -0
  36. krims_pip/node_project/src/mic.js +220 -0
  37. krims_pip/node_project/src/modes.js +92 -0
  38. krims_pip/node_project/src/telemetry-server.js +855 -0
  39. krims_pip/node_project/src/ui/banner.js +204 -0
  40. krims_pip/node_project/src/ui/dashboard.html +834 -0
  41. krims_pip/node_project/src/ui/spinner.js +43 -0
  42. krims_pip/node_project/src/ui/theme.js +624 -0
  43. krims_pip/node_project/src/updater.js +196 -0
@@ -0,0 +1,157 @@
1
+ import { getGitDiff, runGitCommit } from "../git.js";
2
+ import { getAIConfig } from "../config.js";
3
+ import { routePrompt } from "../ai/router.js";
4
+ import { MODES, DEFAULT_MODE } from "../modes.js";
5
+ import { colors, label } from "../ui/theme.js";
6
+ import { createInterface } from "node:readline/promises";
7
+
8
+ export default {
9
+ name: "commit",
10
+ description: "Generate conventional commit message from git diff and commit changes",
11
+ aliases: [],
12
+ options: [],
13
+ async executeCLI(args, options) {
14
+ const rl = createInterface({
15
+ input: process.stdin,
16
+ output: process.stdout,
17
+ });
18
+ try {
19
+ const { diff, isStaged } = await getGitDiff();
20
+ if (!diff) {
21
+ console.log("\n" + label.system + " " + colors.warning("No staged or unstaged changes detected. Stage your files using 'git add' first.\n"));
22
+ rl.close();
23
+ return;
24
+ }
25
+
26
+ if (!isStaged) {
27
+ const stageAnswer = await rl.question("\n" + label.system + " " + colors.warning("No staged changes found. Do you want to stage all changes automatically? [y/N]: "));
28
+
29
+ if (stageAnswer.toLowerCase().trim() === "y" || stageAnswer.toLowerCase().trim() === "yes") {
30
+ const { exec } = await import("node:child_process");
31
+ const { promisify } = await import("node:util");
32
+ const execAsync = promisify(exec);
33
+ await execAsync("git add .");
34
+ console.log(label.system + " " + colors.success("Staged all changes successfully."));
35
+ } else {
36
+ console.log("\n" + label.system + " " + colors.muted("Aborted. Please stage files using 'git add' first.\n"));
37
+ rl.close();
38
+ return;
39
+ }
40
+ }
41
+
42
+ const aiConfig = await getAIConfig();
43
+ const mode = MODES[DEFAULT_MODE];
44
+
45
+ console.log("");
46
+ console.log(label.system + " " + colors.brand("Reading git diff and generating conventional commit message..."));
47
+ console.log("");
48
+
49
+ const systemPrompt = "You are an expert developer assistant. Generate a concise, clear, and professional conventional commit message (e.g., 'feat: add login page', 'fix: resolve buffer overflow') based on the provided git diff. Output ONLY the commit message itself on a single line, with absolutely no backticks, markdown, explanations, prefix, or introductory text.";
50
+ const userPrompt = `Here is the git diff:\n\n${diff}`;
51
+
52
+ let firstToken = true;
53
+ let commitMessage = "";
54
+ const onToken = (token) => {
55
+ if (firstToken) {
56
+ firstToken = false;
57
+ process.stdout.write(label.krims + " Suggested Commit Message: " + colors.success(token));
58
+ } else {
59
+ process.stdout.write(colors.success(token));
60
+ }
61
+ commitMessage += token;
62
+ };
63
+
64
+ const result = await routePrompt(userPrompt, mode.systemPrompt, aiConfig, onToken);
65
+ console.log("\n");
66
+
67
+ const cleanMessage = result.text.trim().replace(/^`+|`+$/g, ""); // strip quotes/backticks
68
+
69
+ const answer = await rl.question(colors.muted("Commit with this message? [Y/n]: "));
70
+
71
+ if (answer.toLowerCase().trim() === "n" || answer.toLowerCase().trim() === "no") {
72
+ console.log("\n" + label.system + " " + colors.muted("Commit aborted.\n"));
73
+ rl.close();
74
+ return;
75
+ }
76
+
77
+ console.log("\n" + label.system + " " + colors.brand("Executing git commit..."));
78
+ const output = await runGitCommit(cleanMessage);
79
+ console.log("\n" + colors.success(output) + "\n");
80
+
81
+ } catch (err) {
82
+ console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
83
+ } finally {
84
+ rl.close();
85
+ }
86
+ },
87
+ async executeChat(args, ctx) {
88
+ const { exec } = await import("node:child_process");
89
+ const { promisify } = await import("node:util");
90
+ const execAsync = promisify(exec);
91
+
92
+ try {
93
+ const { diff, isStaged } = await getGitDiff();
94
+ if (!diff) {
95
+ console.log("\n" + label.system + " " + colors.warning("No staged or unstaged changes detected. Stage your files using 'git add' first.\n"));
96
+ return;
97
+ }
98
+
99
+ if (!isStaged) {
100
+ ctx.rl.pause();
101
+ const stageAnswer = await new Promise((resolve) => {
102
+ ctx.rl.question(colors.warning("\nNo staged changes found. Do you want to stage all changes automatically? [y/N]: "), resolve);
103
+ });
104
+ ctx.rl.resume();
105
+
106
+ if (stageAnswer.toLowerCase().trim() === "y" || stageAnswer.toLowerCase().trim() === "yes") {
107
+ await execAsync("git add .");
108
+ console.log(label.system + " " + colors.success("Staged all changes successfully."));
109
+ } else {
110
+ console.log("\n" + label.system + " " + colors.muted("Aborted. Please stage files using 'git add' first.\n"));
111
+ return;
112
+ }
113
+ }
114
+
115
+ console.log("\n" + label.system + " " + colors.brand("Reading git diff and generating conventional commit message..."));
116
+ console.log("");
117
+
118
+ const systemPrompt = "You are an expert developer assistant. Generate a concise, clear, and professional conventional commit message (e.g., 'feat: add login page', 'fix: resolve buffer overflow') based on the provided git diff. Output ONLY the commit message itself on a single line, with absolutely no backticks, markdown, explanations, prefix, or introductory text.";
119
+ const userPrompt = `Here is the git diff:\n\n${diff}`;
120
+
121
+ let firstToken = true;
122
+ let commitMessage = "";
123
+ const onToken = (token) => {
124
+ if (firstToken) {
125
+ firstToken = false;
126
+ process.stdout.write(label.krims + " Suggested Commit Message: " + colors.success(token));
127
+ } else {
128
+ process.stdout.write(colors.success(token));
129
+ }
130
+ commitMessage += token;
131
+ };
132
+
133
+ const result = await routePrompt(userPrompt, systemPrompt, ctx.aiConfig, onToken);
134
+ console.log("\n");
135
+
136
+ const cleanMessage = result.text.trim().replace(/^`+|`+$/g, ""); // strip quotes/backticks
137
+
138
+ ctx.rl.pause();
139
+ const answer = await new Promise((resolve) => {
140
+ ctx.rl.question(colors.muted("Commit with this message? [Y/n]: "), resolve);
141
+ });
142
+ ctx.rl.resume();
143
+
144
+ if (answer.toLowerCase().trim() === "n" || answer.toLowerCase().trim() === "no") {
145
+ console.log("\n" + label.system + " " + colors.muted("Commit aborted.\n"));
146
+ return;
147
+ }
148
+
149
+ console.log("\n" + label.system + " " + colors.brand("Executing git commit..."));
150
+ const output = await runGitCommit(cleanMessage);
151
+ console.log("\n" + colors.success(output) + "\n");
152
+
153
+ } catch (err) {
154
+ console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
155
+ }
156
+ }
157
+ };
@@ -0,0 +1,59 @@
1
+ import { colors, label, separator, keyValue } from "../ui/theme.js";
2
+
3
+ export default {
4
+ name: "dashboard",
5
+ description: "Launch visual telemetry dashboard HUD in browser",
6
+ aliases: ["telemetry"],
7
+ options: [
8
+ {
9
+ flags: "-p, --port <port>",
10
+ description: "Port to run the dashboard server on",
11
+ defaultValue: "5050"
12
+ }
13
+ ],
14
+ async executeCLI(args, options) {
15
+ const { startTelemetryServer, openBrowser } = await import("../telemetry-server.js");
16
+ const parsedPort = parseInt(options.port, 10) || 5050;
17
+
18
+ console.log("");
19
+ console.log(label.system + " " + colors.brand("Initializing Krims Code Visual Telemetry HUD..."));
20
+
21
+ try {
22
+ const { port } = await startTelemetryServer(parsedPort);
23
+ const url = `http://localhost:${port}`;
24
+ console.log(" " + colors.success(`✓ Telemetry Server active at ${colors.accent(url)}`));
25
+ console.log(" " + colors.muted("Press Ctrl+C to terminate dashboard server."));
26
+ console.log("");
27
+
28
+ openBrowser(url);
29
+ } catch (err) {
30
+ console.log("\n" + label.error + " " + colors.danger(`Failed to start telemetry server: ${err.message}\n`));
31
+ process.exit(1);
32
+ }
33
+ },
34
+ async executeChat(args, ctx) {
35
+ const { startDashboardServer } = await import("../dashboard.js");
36
+ const { exec } = await import("node:child_process");
37
+
38
+ try {
39
+ const { port } = await startDashboardServer();
40
+ console.log("\n" + label.system + " " + colors.brand("📊 Krims Code WEB TELEMETRY DASHBOARD"));
41
+ console.log(separator("─"));
42
+ console.log(keyValue(" Status", colors.success("ONLINE")));
43
+ console.log(keyValue(" Local URL", `http://localhost:${port}`));
44
+ console.log("");
45
+ console.log(" " + colors.muted("Launching browser companion automatically..."));
46
+
47
+ let startCmd = `start http://localhost:${port}`;
48
+ if (process.platform === "darwin") {
49
+ startCmd = `open http://localhost:${port}`;
50
+ } else if (process.platform === "linux") {
51
+ startCmd = `xdg-open http://localhost:${port}`;
52
+ }
53
+ exec(startCmd);
54
+ console.log("\n" + label.system + " " + colors.success("✓ Dashboard launched. Press Ctrl+C in this session to stop dashboard at exit.\n"));
55
+ } catch (err) {
56
+ console.log("\n" + label.error + " " + colors.danger("Failed to start dashboard server: " + err.message + "\n"));
57
+ }
58
+ }
59
+ };
@@ -0,0 +1,349 @@
1
+ import cp from "node:child_process";
2
+ import { getAIConfig } from "../config.js";
3
+ import { colors, separator, keyValue, label } from "../ui/theme.js";
4
+
5
+ // Helper to execute git CLI commands using child_process.exec dynamically so it's mockable in tests
6
+ function execGit(args) {
7
+ return new Promise((resolve, reject) => {
8
+ cp.exec(`git ${args}`, (err, stdout, stderr) => {
9
+ if (err) {
10
+ reject(err);
11
+ } else {
12
+ resolve(stdout.trim());
13
+ }
14
+ });
15
+ });
16
+ }
17
+
18
+ // Parses GitHub owner and repo from remote git URL
19
+ function parseGithubUrl(url) {
20
+ if (!url) return null;
21
+ const cleanUrl = url.trim();
22
+
23
+ // SSH format: git@github.com:owner/repo.git or git@github.com:owner/repo
24
+ const sshRegex = /git@github\.com:([^/]+)\/([^.]+)(?:\.git)?$/;
25
+ const sshMatch = cleanUrl.match(sshRegex);
26
+ if (sshMatch) {
27
+ return { owner: sshMatch[1], repo: sshMatch[2] };
28
+ }
29
+
30
+ // HTTPS format: https://github.com/owner/repo.git or https://github.com/owner/repo
31
+ const httpsRegex = /https?:\/\/github\.com\/([^/]+)\/([^.]+)(?:\.git)?$/;
32
+ const httpsMatch = cleanUrl.match(httpsRegex);
33
+ if (httpsMatch) {
34
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
35
+ }
36
+
37
+ // Fallback pattern if URL is slightly different but still valid github URL
38
+ const fallbackRegex = /github\.com[\/:][^\/]+\/[^\/]+/;
39
+ if (fallbackRegex.test(cleanUrl)) {
40
+ const parts = cleanUrl.split(/github\.com[\/:]/)[1].split('/');
41
+ if (parts.length >= 2) {
42
+ const owner = parts[0];
43
+ const repo = parts[1].replace(/\.git$/, '');
44
+ return { owner, repo };
45
+ }
46
+ }
47
+
48
+ // Simple fallback parsing for other cases (e.g. "owner/repo")
49
+ const parts = cleanUrl.split('/');
50
+ if (parts.length === 2) {
51
+ return { owner: parts[0], repo: parts[1] };
52
+ }
53
+
54
+ return null;
55
+ }
56
+
57
+ // Dynamically extracts owner/repo name from git remote configuration
58
+ async function getGithubRepoInfo() {
59
+ let url = "";
60
+ try {
61
+ url = await execGit("config --get remote.origin.url");
62
+ } catch {
63
+ try {
64
+ const output = await execGit("remote -v");
65
+ const lines = output.split('\n');
66
+ const originLine = lines.find(l => l.includes("origin") && l.includes("github.com"));
67
+ if (originLine) {
68
+ const match = originLine.match(/origin\s+(.*?)\s+\((?:fetch|push)\)/);
69
+ if (match) {
70
+ url = match[1].trim();
71
+ }
72
+ }
73
+ } catch {
74
+ // Ignored
75
+ }
76
+ }
77
+
78
+ if (!url) {
79
+ throw new Error("Could not retrieve remote.origin.url. Ensure you are in a git repository with a GitHub remote configured.");
80
+ }
81
+
82
+ const info = parseGithubUrl(url);
83
+ if (!info) {
84
+ throw new Error(`Could not parse GitHub owner and repository from remote URL: ${url}`);
85
+ }
86
+ return info;
87
+ }
88
+
89
+ // Retrieves GITHUB token from krims-code config or env variables
90
+ async function getGithubToken() {
91
+ const aiConfig = await getAIConfig();
92
+ const token = aiConfig.GITHUB_API_KEY ||
93
+ aiConfig.GITHUB_PERSONAL_ACCESS_TOKEN ||
94
+ process.env.GITHUB_TOKEN ||
95
+ process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
96
+ return token || null;
97
+ }
98
+
99
+ // Runs standard github API fetch with appropriate auth headers
100
+ async function githubFetch(endpoint, token) {
101
+ const headers = {
102
+ Accept: "application/vnd.github.v3+json",
103
+ "User-Agent": "krims-code-cli"
104
+ };
105
+ if (token) {
106
+ headers["Authorization"] = `Bearer ${token}`;
107
+ }
108
+ const url = `https://api.github.com${endpoint}`;
109
+ try {
110
+ const res = await fetch(url, { headers });
111
+ if (!res.ok) {
112
+ if (res.status === 401 || res.status === 403) {
113
+ throw new Error(`GitHub API returned ${res.status} (${res.statusText}). If this is a private repository or you are hit by rate limits, please check your token.`);
114
+ }
115
+ throw new Error(`GitHub API returned ${res.status}: ${res.statusText}`);
116
+ }
117
+ return await res.json();
118
+ } catch (err) {
119
+ throw new Error(`Failed to fetch from GitHub: ${err.message}`);
120
+ }
121
+ }
122
+
123
+ // Formats and retrieves local git information
124
+ async function getLocalGitStatus() {
125
+ try {
126
+ const branch = await execGit("rev-parse --abbrev-ref HEAD");
127
+
128
+ let modifiedCount = 0;
129
+ try {
130
+ const statusOutput = await execGit("status --porcelain");
131
+ modifiedCount = statusOutput.split('\n').map(l => l.trim()).filter(Boolean).length;
132
+ } catch {
133
+ // status porcelain might fail if not in git repo, but getGithubRepoInfo would have already thrown
134
+ }
135
+
136
+ let syncStatus = "No upstream branch configured";
137
+ let tracking = null;
138
+
139
+ try {
140
+ tracking = await execGit("rev-parse --abbrev-ref @{u}");
141
+ const countOutput = await execGit(`rev-list --left-right --count HEAD...${tracking}`);
142
+ const [ahead, behind] = countOutput.trim().split(/\s+/).map(Number);
143
+ if (ahead > 0 && behind > 0) {
144
+ syncStatus = `Ahead by ${ahead}, Behind by ${behind} commits`;
145
+ } else if (ahead > 0) {
146
+ syncStatus = `Ahead by ${ahead} commit${ahead > 1 ? "s" : ""}`;
147
+ } else if (behind > 0) {
148
+ syncStatus = `Behind by ${behind} commit${behind > 1 ? "s" : ""}`;
149
+ } else {
150
+ syncStatus = "Up to date with remote";
151
+ }
152
+ } catch {
153
+ // No upstream tracking branch
154
+ }
155
+
156
+ return { branch, modifiedCount, tracking, syncStatus };
157
+ } catch (err) {
158
+ throw new Error(`Git CLI error: ${err.message}`);
159
+ }
160
+ }
161
+
162
+ // Display help menu
163
+ function showGithubHelp() {
164
+ console.log("");
165
+ console.log(colors.brand(" ⚡ GITHUB COMMAND USAGE"));
166
+ console.log(separator("─"));
167
+ console.log(keyValue(" Usage", "Krims Code github <subcommand> [options]"));
168
+ console.log(keyValue(" Chat Usage", "/github <subcommand> [options]"));
169
+ console.log("");
170
+ console.log(colors.accent(" ◈ Subcommands:"));
171
+ console.log(` ${colors.success("status")} : Show remote repository metadata, local git info, and recent CI runs`);
172
+ console.log(` ${colors.success("issues")} : List issues (use --state=open|closed to filter, default: open)`);
173
+ console.log(` ${colors.success("prs")} : List open pull requests`);
174
+ console.log("");
175
+ console.log(colors.accent(" ◈ Options:"));
176
+ console.log(` ${colors.success("--state=<state>")} : Filter issues by state (open, closed, all)`);
177
+ console.log("");
178
+ }
179
+
180
+ // Main runner for github command operations
181
+ async function runGithubCommand(subcommand, options = {}) {
182
+ if (!subcommand || subcommand === "help") {
183
+ showGithubHelp();
184
+ return;
185
+ }
186
+
187
+ // Retrieve Git repo details
188
+ const { owner, repo } = await getGithubRepoInfo();
189
+
190
+ // Retrieve token
191
+ const token = await getGithubToken();
192
+ if (!token) {
193
+ console.log(colors.warning(" ⚠ Warning: GITHUB_TOKEN or GITHUB_API_KEY not configured. Proceeding anonymously (rate limits apply)."));
194
+ }
195
+
196
+ if (subcommand === "status") {
197
+ console.log("\n" + label.system + " " + colors.brand("Fetching remote metadata and local status..."));
198
+
199
+ // Fetch remote metadata and runs concurrently/sequentially
200
+ const repoData = await githubFetch(`/repos/${owner}/${repo}`, token);
201
+ let runsData = { workflow_runs: [] };
202
+ try {
203
+ runsData = await githubFetch(`/repos/${owner}/${repo}/actions/runs?per_page=5`, token);
204
+ } catch {
205
+ // Actions runs might fail if actions are disabled or not accessible
206
+ }
207
+
208
+ const localStatus = await getLocalGitStatus();
209
+
210
+ console.log("");
211
+ console.log(colors.brand(` ⚡ GITHUB WORKSPACE: ${owner}/${repo}`));
212
+ console.log(separator("─"));
213
+ console.log(keyValue(" Stars", `★ ${repoData.stargazers_count || 0}`));
214
+ console.log(keyValue(" Forks", `⑂ ${repoData.forks_count || 0}`));
215
+ console.log(keyValue(" Open Issues/PRs", `${repoData.open_issues_count || 0}`));
216
+ console.log("");
217
+
218
+ console.log(colors.accent(" ◈ Local Git Status:"));
219
+ console.log(keyValue(" Branch", localStatus.branch));
220
+ console.log(keyValue(" Modified Files", String(localStatus.modifiedCount)));
221
+ console.log(keyValue(" Sync Status", localStatus.syncStatus));
222
+ console.log("");
223
+
224
+ console.log(colors.accent(" ◈ Recent Workflow Runs:"));
225
+ const runs = runsData.workflow_runs || [];
226
+ if (runs.length === 0) {
227
+ console.log(" No recent workflow runs found.");
228
+ } else {
229
+ for (const run of runs.slice(0, 5)) {
230
+ let statusSymbol = colors.muted("○");
231
+ let statusStr = run.status;
232
+ if (run.status === "completed") {
233
+ if (run.conclusion === "success") {
234
+ statusSymbol = colors.success("✓");
235
+ statusStr = colors.success("success");
236
+ } else if (run.conclusion === "failure") {
237
+ statusSymbol = colors.danger("✗");
238
+ statusStr = colors.danger("failed");
239
+ } else {
240
+ statusSymbol = colors.warning("⚠");
241
+ statusStr = colors.warning(run.conclusion || "unknown");
242
+ }
243
+ } else {
244
+ statusSymbol = colors.accent("◈");
245
+ statusStr = colors.accent(run.status);
246
+ }
247
+ console.log(` ${statusSymbol} ${colors.text(run.name)} (${colors.muted(run.head_branch)}) - ${statusStr} [${run.event}]`);
248
+ }
249
+ }
250
+ console.log("");
251
+
252
+ } else if (subcommand === "issues") {
253
+ const state = options.state || "open";
254
+ console.log("\n" + label.system + " " + colors.brand(`Fetching remote issues (state: ${state})...`));
255
+
256
+ const rawIssues = await githubFetch(`/repos/${owner}/${repo}/issues?state=${state}&per_page=15`, token);
257
+ const issues = rawIssues.filter(item => !item.pull_request);
258
+
259
+ console.log("");
260
+ console.log(colors.brand(` ⚡ GITHUB ISSUES (State: ${state.toUpperCase()})`));
261
+ console.log(separator("─"));
262
+ if (issues.length === 0) {
263
+ console.log(` No ${state} issues found.`);
264
+ } else {
265
+ for (const issue of issues.slice(0, 10)) {
266
+ const numberStr = colors.accent(`#${issue.number}`);
267
+ const titleStr = colors.text(issue.title);
268
+ const stateColor = issue.state === "open" ? colors.success : colors.muted;
269
+ console.log(` ${numberStr} ${titleStr}`);
270
+ console.log(` ${colors.muted("State:")} ${stateColor(issue.state)} | ${colors.muted("Author:")} ${issue.user?.login || "unknown"} | ${colors.muted("Comments:")} ${issue.comments}`);
271
+ console.log(` ${colors.dim(issue.html_url)}`);
272
+ console.log("");
273
+ }
274
+ }
275
+
276
+ } else if (subcommand === "prs") {
277
+ console.log("\n" + label.system + " " + colors.brand("Fetching remote open pull requests..."));
278
+
279
+ const prs = await githubFetch(`/repos/${owner}/${repo}/pulls?state=open&per_page=15`, token);
280
+
281
+ console.log("");
282
+ console.log(colors.brand(" ⚡ GITHUB OPEN PULL REQUESTS"));
283
+ console.log(separator("─"));
284
+ if (prs.length === 0) {
285
+ console.log(" No open pull requests found.");
286
+ } else {
287
+ for (const pr of prs.slice(0, 10)) {
288
+ const numberStr = colors.accent(`#${pr.number}`);
289
+ const titleStr = colors.text(pr.title);
290
+ console.log(` ${numberStr} ${titleStr}`);
291
+ console.log(` ${colors.muted("Branch:")} ${colors.accent2(pr.head?.ref || "unknown")} ➔ ${colors.muted(pr.base?.ref || "unknown")} | ${colors.muted("Author:")} ${pr.user?.login || "unknown"}`);
292
+ console.log(` ${colors.dim(pr.html_url)}`);
293
+ console.log("");
294
+ }
295
+ }
296
+ } else {
297
+ throw new Error(`Unknown subcommand: ${subcommand}. Support status, issues, prs.`);
298
+ }
299
+ }
300
+
301
+ // Parses slash chat command args manually
302
+ function parseArgs(args) {
303
+ const parsed = {
304
+ subcommand: "",
305
+ options: { state: "open" }
306
+ };
307
+ for (const arg of args) {
308
+ if (arg.startsWith("--")) {
309
+ const parts = arg.slice(2).split("=");
310
+ const key = parts[0].toLowerCase();
311
+ parsed.options[key] = parts[1] || true;
312
+ } else if (arg.startsWith("-")) {
313
+ // Short flag support if needed
314
+ } else if (!parsed.subcommand) {
315
+ parsed.subcommand = arg.toLowerCase();
316
+ }
317
+ }
318
+ return parsed;
319
+ }
320
+
321
+ export default {
322
+ name: "github",
323
+ description: "Orchestrate Git & GitHub workspace status, issues, and pull requests",
324
+ aliases: ["gh"],
325
+ signature: "github [subcommand]",
326
+ options: [
327
+ {
328
+ flags: "--state <state>",
329
+ description: "State of the issues to filter (open, closed, all)",
330
+ defaultValue: "open"
331
+ }
332
+ ],
333
+ async executeCLI(args, options) {
334
+ try {
335
+ const subcommand = args[0] || "";
336
+ await runGithubCommand(subcommand, options);
337
+ } catch (err) {
338
+ console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
339
+ }
340
+ },
341
+ async executeChat(args, ctx) {
342
+ try {
343
+ const parsed = parseArgs(args);
344
+ await runGithubCommand(parsed.subcommand, parsed.options);
345
+ } catch (err) {
346
+ console.log("\n" + label.error + " " + colors.danger(err.message) + "\n");
347
+ }
348
+ }
349
+ };
@@ -0,0 +1,67 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ async function loadCommands() {
9
+ const commands = [];
10
+ const files = fs.readdirSync(__dirname);
11
+
12
+ for (const file of files) {
13
+ if (file === 'index.js' || !file.endsWith('.js') || file.startsWith('.')) {
14
+ continue;
15
+ }
16
+
17
+ const filePath = path.join(__dirname, file);
18
+ const fileUrl = new URL(`file://${filePath}`).href;
19
+ const module = await import(fileUrl);
20
+ const cmd = module.default || module.command;
21
+
22
+ if (cmd && cmd.name) {
23
+ commands.push(cmd);
24
+ }
25
+ }
26
+
27
+ return commands;
28
+ }
29
+
30
+ class CommandRegistry {
31
+ constructor() {
32
+ this.commands = new Map();
33
+ this.aliases = new Map();
34
+ this.allList = [];
35
+ this.loaded = false;
36
+ }
37
+
38
+ async load() {
39
+ if (this.loaded) return;
40
+ this.commands.clear();
41
+ this.aliases.clear();
42
+ this.allList = [];
43
+
44
+ const loaded = await loadCommands();
45
+ for (const cmd of loaded) {
46
+ this.commands.set(cmd.name.toLowerCase(), cmd);
47
+ this.allList.push(cmd);
48
+ if (cmd.aliases && Array.isArray(cmd.aliases)) {
49
+ for (const alias of cmd.aliases) {
50
+ this.aliases.set(alias.toLowerCase(), cmd);
51
+ }
52
+ }
53
+ }
54
+ this.loaded = true;
55
+ }
56
+
57
+ get(nameOrAlias) {
58
+ const key = nameOrAlias.toLowerCase();
59
+ return this.commands.get(key) || this.aliases.get(key);
60
+ }
61
+
62
+ getAll() {
63
+ return this.allList;
64
+ }
65
+ }
66
+
67
+ export const registry = new CommandRegistry();