@cosmicstack/mercury-agent 0.2.7 → 0.3.0

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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync12 } from "fs";
4
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync13, existsSync as existsSync16 } from "fs";
5
5
  import { fileURLToPath } from "url";
6
6
  import { dirname as dirname3, join as join11 } from "path";
7
7
  import { Command } from "commander";
@@ -73,6 +73,10 @@ function getDefaultConfig() {
73
73
  streaming: getEnvBool("TELEGRAM_STREAMING", true)
74
74
  }
75
75
  },
76
+ github: {
77
+ username: getEnv("GITHUB_USERNAME", ""),
78
+ email: getEnv("GITHUB_EMAIL", "")
79
+ },
76
80
  memory: {
77
81
  dir: getEnv("MEMORY_DIR", join(home, "memory")),
78
82
  shortTermMaxMessages: getEnvNum("SHORT_TERM_MAX_MESSAGES", 20)
@@ -2334,8 +2338,8 @@ var PermissionManager = class {
2334
2338
  clearElevation() {
2335
2339
  this.elevatedCommands.clear();
2336
2340
  }
2337
- isElevated(tool25) {
2338
- if (this.elevatedCommands.has(tool25)) return true;
2341
+ isElevated(tool30) {
2342
+ if (this.elevatedCommands.has(tool30)) return true;
2339
2343
  return false;
2340
2344
  }
2341
2345
  isShellElevated() {
@@ -3281,15 +3285,19 @@ function createGitAddTool() {
3281
3285
  import { tool as tool22 } from "ai";
3282
3286
  import { z as z22 } from "zod";
3283
3287
  import { execSync as execSync6 } from "child_process";
3288
+ var CO_AUTHOR = "Mercury <mercury@cosmicstack.org>";
3284
3289
  function createGitCommitTool() {
3285
3290
  return tool22({
3286
- description: "Record changes to the repository. Creates a new commit with staged changes.",
3291
+ description: "Record changes to the repository. Creates a new commit with staged changes. Automatically includes a Co-authored-by trailer for attribution.",
3287
3292
  parameters: z22.object({
3288
3293
  message: z22.string().describe("Commit message")
3289
3294
  }),
3290
3295
  execute: async ({ message }) => {
3291
3296
  try {
3292
- const escapedMsg = message.replace(/"/g, '\\"');
3297
+ const fullMessage = `${message}
3298
+
3299
+ Co-authored-by: ${CO_AUTHOR}`;
3300
+ const escapedMsg = fullMessage.replace(/"/g, '\\"');
3293
3301
  const result = execSync6(`git commit -m "${escapedMsg}"`, { encoding: "utf-8", timeout: 1e4 });
3294
3302
  return result.trim() || "Committed successfully.";
3295
3303
  } catch (err) {
@@ -3336,9 +3344,302 @@ Ask the user for permission. If they approve, try again. If they say "always", u
3336
3344
  });
3337
3345
  }
3338
3346
 
3339
- // src/capabilities/web/fetch-url.ts
3347
+ // src/capabilities/github/create-pr.ts
3340
3348
  import { tool as tool24 } from "ai";
3341
3349
  import { z as z24 } from "zod";
3350
+
3351
+ // src/utils/github.ts
3352
+ var GITHUB_API = "https://api.github.com";
3353
+ var cachedToken = null;
3354
+ function setGitHubToken(token) {
3355
+ cachedToken = token;
3356
+ }
3357
+ function getGitHubToken() {
3358
+ if (cachedToken) return cachedToken;
3359
+ return process.env.GITHUB_TOKEN || null;
3360
+ }
3361
+ function isGitHubConfigured() {
3362
+ return !!getGitHubToken();
3363
+ }
3364
+ async function githubRequest(path3, options = {}) {
3365
+ const token = getGitHubToken();
3366
+ if (!token) {
3367
+ throw new Error("GITHUB_TOKEN not configured. Run mercury doctor to set it up.");
3368
+ }
3369
+ const url = path3.startsWith("http") ? path3 : `${GITHUB_API}${path3}`;
3370
+ const headers = {
3371
+ "Authorization": `Bearer ${token}`,
3372
+ "Accept": "application/vnd.github.v3+json",
3373
+ "User-Agent": "Mercury-Agent",
3374
+ ...options.headers
3375
+ };
3376
+ const fetchOptions = {
3377
+ method: options.method || "GET",
3378
+ headers
3379
+ };
3380
+ if (options.body) {
3381
+ fetchOptions.body = JSON.stringify(options.body);
3382
+ headers["Content-Type"] = "application/json";
3383
+ }
3384
+ logger.info({ method: fetchOptions.method, path: path3 }, "GitHub API request");
3385
+ const response = await fetch(url, fetchOptions);
3386
+ const remaining = response.headers.get("x-ratelimit-remaining");
3387
+ if (remaining && parseInt(remaining, 10) < 100) {
3388
+ logger.warn({ remaining }, "GitHub API rate limit running low");
3389
+ }
3390
+ if (!response.ok) {
3391
+ const body = await response.text();
3392
+ throw new Error(`GitHub API ${response.status}: ${body.slice(0, 500)}`);
3393
+ }
3394
+ if (response.status === 204) return null;
3395
+ return response.json();
3396
+ }
3397
+ function parseRepo(remoteUrl) {
3398
+ const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/([^.\s]+)(?:\.git)?$/);
3399
+ if (sshMatch) return { owner: sshMatch[1], repo: sshMatch[2] };
3400
+ const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/([^.\s]+)(?:\.git)?$/);
3401
+ if (httpsMatch) return { owner: httpsMatch[1], repo: httpsMatch[2] };
3402
+ return null;
3403
+ }
3404
+ async function getCurrentRepo() {
3405
+ try {
3406
+ const { execSync: execSync9 } = await import("child_process");
3407
+ const remoteUrl = execSync9("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
3408
+ return parseRepo(remoteUrl);
3409
+ } catch {
3410
+ return null;
3411
+ }
3412
+ }
3413
+
3414
+ // src/capabilities/github/create-pr.ts
3415
+ function createCreatePrTool() {
3416
+ return tool24({
3417
+ description: "Create a pull request on GitHub. Requires GITHUB_TOKEN to be configured.",
3418
+ parameters: z24.object({
3419
+ title: z24.string().describe("PR title"),
3420
+ body: z24.string().describe("PR description (markdown supported)").default(""),
3421
+ head: z24.string().describe("The branch containing the changes"),
3422
+ base: z24.string().describe("The branch to merge into").default("main"),
3423
+ draft: z24.boolean().describe("Create as draft PR").default(false)
3424
+ }),
3425
+ execute: async ({ title, body, head, base, draft }) => {
3426
+ try {
3427
+ const repo = await getCurrentRepo();
3428
+ if (!repo) return "Error: Could not detect GitHub repository. Make sure you are in a git repo with a GitHub remote.";
3429
+ const result = await githubRequest(`/repos/${repo.owner}/${repo.repo}/pulls`, {
3430
+ method: "POST",
3431
+ body: {
3432
+ title,
3433
+ body,
3434
+ head,
3435
+ base,
3436
+ draft
3437
+ }
3438
+ });
3439
+ return `PR created: ${result.html_url}
3440
+ #${result.number}: ${result.title}
3441
+ ${draft ? "(draft)" : ""} ${result.state}`;
3442
+ } catch (err) {
3443
+ return `Error creating PR: ${err.message}`;
3444
+ }
3445
+ }
3446
+ });
3447
+ }
3448
+
3449
+ // src/capabilities/github/review-pr.ts
3450
+ import { tool as tool25 } from "ai";
3451
+ import { z as z25 } from "zod";
3452
+ function createReviewPrTool() {
3453
+ return tool25({
3454
+ description: "Get details of a pull request including the diff. Reviews the PR and returns the title, body, changed files, and diff. Optionally post a review comment.",
3455
+ parameters: z25.object({
3456
+ number: z25.number().describe("PR number"),
3457
+ comment: z25.string().describe("Review comment to post on the PR (optional)").optional()
3458
+ }),
3459
+ execute: async ({ number, comment }) => {
3460
+ try {
3461
+ const repo = await getCurrentRepo();
3462
+ if (!repo) return "Error: Could not detect GitHub repository.";
3463
+ const pr = await githubRequest(`/repos/${repo.owner}/${repo.repo}/pulls/${number}`);
3464
+ if (!pr) return `Error: PR #${number} not found.`;
3465
+ const diff = await githubRequest(`/repos/${repo.owner}/${repo.repo}/pulls/${number}`, {
3466
+ headers: { "Accept": "application/vnd.github.v3.diff" }
3467
+ });
3468
+ let summary = `PR #${pr.number}: ${pr.title}
3469
+ `;
3470
+ summary += `Author: ${pr.user?.login}
3471
+ `;
3472
+ summary += `State: ${pr.state} (${pr.merged ? "merged" : pr.mergeable_state || "unknown"})
3473
+ `;
3474
+ summary += `Branch: ${pr.head.ref} \u2192 ${pr.base.ref}
3475
+ `;
3476
+ summary += `Changed files: ${pr.changed_files} | Additions: +${pr.additions} | Deletions: -${pr.deletions}
3477
+
3478
+ `;
3479
+ if (pr.body) {
3480
+ summary += `Description:
3481
+ ${pr.body.slice(0, 2e3)}
3482
+
3483
+ `;
3484
+ }
3485
+ if (typeof diff === "string") {
3486
+ const diffLines = diff.split("\n");
3487
+ const maxDiffLines = 200;
3488
+ summary += `Diff (first ${Math.min(diffLines.length, maxDiffLines)} of ${diffLines.length} lines):
3489
+ `;
3490
+ summary += diffLines.slice(0, maxDiffLines).join("\n");
3491
+ if (diffLines.length > maxDiffLines) {
3492
+ summary += `
3493
+
3494
+ ... (${diffLines.length - maxDiffLines} more lines)`;
3495
+ }
3496
+ } else {
3497
+ summary += `(Diff not available in text format)`;
3498
+ }
3499
+ if (comment) {
3500
+ try {
3501
+ await githubRequest(`/repos/${repo.owner}/${repo.repo}/pulls/${number}/reviews`, {
3502
+ method: "POST",
3503
+ body: {
3504
+ body: comment,
3505
+ event: "COMMENT"
3506
+ }
3507
+ });
3508
+ summary += `
3509
+
3510
+ Review comment posted.`;
3511
+ } catch (err) {
3512
+ summary += `
3513
+
3514
+ Failed to post review comment: ${err.message}`;
3515
+ }
3516
+ }
3517
+ return summary;
3518
+ } catch (err) {
3519
+ return `Error reviewing PR: ${err.message}`;
3520
+ }
3521
+ }
3522
+ });
3523
+ }
3524
+
3525
+ // src/capabilities/github/list-issues.ts
3526
+ import { tool as tool26 } from "ai";
3527
+ import { z as z26 } from "zod";
3528
+ function createListIssuesTool() {
3529
+ return tool26({
3530
+ description: "List open GitHub issues for the current repository. Requires GITHUB_TOKEN.",
3531
+ parameters: z26.object({
3532
+ state: z26.enum(["open", "closed", "all"]).describe("Filter by issue state").default("open"),
3533
+ labels: z26.string().describe("Comma-separated label names to filter by (optional)").optional(),
3534
+ limit: z26.number().describe("Maximum number of issues to return").default(10)
3535
+ }),
3536
+ execute: async ({ state, labels, limit }) => {
3537
+ try {
3538
+ const repo = await getCurrentRepo();
3539
+ if (!repo) return "Error: Could not detect GitHub repository.";
3540
+ const params = new URLSearchParams();
3541
+ params.set("state", state);
3542
+ params.set("per_page", String(Math.min(limit, 100)));
3543
+ params.set("sort", "updated");
3544
+ params.set("direction", "desc");
3545
+ if (labels) params.set("labels", labels);
3546
+ const issues = await githubRequest(`/repos/${repo.owner}/${repo.repo}/issues?${params}`);
3547
+ if (!Array.isArray(issues) || issues.length === 0) {
3548
+ return `No ${state} issues found.`;
3549
+ }
3550
+ const lines = issues.map((issue) => {
3551
+ const labelStr = issue.labels?.map((l) => `[${l.name}]`).join(" ") || "";
3552
+ return `#${issue.number} ${issue.title} ${labelStr} (${issue.state}, by ${issue.user?.login})`;
3553
+ });
3554
+ return `Issues in ${repo.owner}/${repo.repo} (${state}):
3555
+ ${lines.join("\n")}`;
3556
+ } catch (err) {
3557
+ return `Error listing issues: ${err.message}`;
3558
+ }
3559
+ }
3560
+ });
3561
+ }
3562
+
3563
+ // src/capabilities/github/create-issue.ts
3564
+ import { tool as tool27 } from "ai";
3565
+ import { z as z27 } from "zod";
3566
+ function createCreateIssueTool() {
3567
+ return tool27({
3568
+ description: "Create a new GitHub issue in the current repository. Requires GITHUB_TOKEN.",
3569
+ parameters: z27.object({
3570
+ title: z27.string().describe("Issue title"),
3571
+ body: z27.string().describe("Issue description (markdown supported)").default(""),
3572
+ labels: z27.array(z27.string()).describe("Label names to apply").optional()
3573
+ }),
3574
+ execute: async ({ title, body, labels }) => {
3575
+ try {
3576
+ const repo = await getCurrentRepo();
3577
+ if (!repo) return "Error: Could not detect GitHub repository.";
3578
+ const payload = { title, body };
3579
+ if (labels && labels.length > 0) payload.labels = labels;
3580
+ const result = await githubRequest(`/repos/${repo.owner}/${repo.repo}/issues`, {
3581
+ method: "POST",
3582
+ body: payload
3583
+ });
3584
+ return `Issue created: ${result.html_url}
3585
+ #${result.number}: ${result.title}`;
3586
+ } catch (err) {
3587
+ return `Error creating issue: ${err.message}`;
3588
+ }
3589
+ }
3590
+ });
3591
+ }
3592
+
3593
+ // src/capabilities/github/github-api.ts
3594
+ import { tool as tool28 } from "ai";
3595
+ import { z as z28 } from "zod";
3596
+ function createGithubApiTool(permissions) {
3597
+ return tool28({
3598
+ description: "Make a raw request to the GitHub API. Use this for any GitHub operation not covered by other tools. GET requests are auto-approved; write operations (POST, PUT, PATCH, DELETE) require approval.",
3599
+ parameters: z28.object({
3600
+ path: z28.string().describe("API path (e.g., /repos/owner/repo/releases)"),
3601
+ method: z28.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).describe("HTTP method").default("GET"),
3602
+ body: z28.string().describe("JSON body for write requests (as a JSON string)").optional()
3603
+ }),
3604
+ execute: async ({ path: path3, method, body }) => {
3605
+ const isWrite = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
3606
+ if (isWrite) {
3607
+ const check = await permissions.checkShellCommand(`github-api ${method} ${path3}`);
3608
+ if (!check.allowed) {
3609
+ if (check.needsApproval) {
3610
+ return `This GitHub API write operation requires approval: ${method} ${path3}
3611
+
3612
+ Tell the user what this does and ask for confirmation. If approved, try again.`;
3613
+ }
3614
+ return `Error: ${check.reason}`;
3615
+ }
3616
+ }
3617
+ try {
3618
+ let parsedBody;
3619
+ if (body) {
3620
+ try {
3621
+ parsedBody = JSON.parse(body);
3622
+ } catch {
3623
+ return "Error: body must be valid JSON.";
3624
+ }
3625
+ }
3626
+ const result = await githubRequest(path3, {
3627
+ method,
3628
+ body: parsedBody
3629
+ });
3630
+ if (result === null) return "Request completed (204 No Content).";
3631
+ if (typeof result === "string") return result;
3632
+ return JSON.stringify(result, null, 2);
3633
+ } catch (err) {
3634
+ return `Error: ${err.message}`;
3635
+ }
3636
+ }
3637
+ });
3638
+ }
3639
+
3640
+ // src/capabilities/web/fetch-url.ts
3641
+ import { tool as tool29 } from "ai";
3642
+ import { z as z29 } from "zod";
3342
3643
  var MAX_CONTENT_LENGTH = 15e3;
3343
3644
  function stripHtml(html) {
3344
3645
  let text = html;
@@ -3371,11 +3672,11 @@ function stripHtml(html) {
3371
3672
  return text;
3372
3673
  }
3373
3674
  function createFetchUrlTool() {
3374
- return tool24({
3675
+ return tool29({
3375
3676
  description: "Fetch a URL and return its content as text. Strips HTML to readable markdown-like format. Useful for reading documentation, APIs, or web pages.",
3376
- parameters: z24.object({
3377
- url: z24.string().describe("The URL to fetch"),
3378
- format: z24.enum(["text", "markdown"]).optional().describe("Output format (default: markdown)")
3677
+ parameters: z29.object({
3678
+ url: z29.string().describe("The URL to fetch"),
3679
+ format: z29.enum(["text", "markdown"]).optional().describe("Output format (default: markdown)")
3379
3680
  }),
3380
3681
  execute: async ({ url, format }) => {
3381
3682
  const outputFormat = format ?? "markdown";
@@ -3497,6 +3798,14 @@ var CapabilityRegistry = class {
3497
3798
  this.tools.git_push = createGitPushTool(this.permissions);
3498
3799
  logger.info("Git tools registered");
3499
3800
  }
3801
+ if (isGitHubConfigured()) {
3802
+ this.tools.create_pr = createCreatePrTool();
3803
+ this.tools.review_pr = createReviewPrTool();
3804
+ this.tools.list_issues = createListIssuesTool();
3805
+ this.tools.create_issue = createCreateIssueTool();
3806
+ this.tools.github_api = createGithubApiTool(this.permissions);
3807
+ logger.info("GitHub tools registered");
3808
+ }
3500
3809
  this.tools.fetch_url = createFetchUrlTool();
3501
3810
  logger.info("Web fetch tool registered");
3502
3811
  }
@@ -4360,6 +4669,17 @@ function maskKey(key) {
4360
4669
  if (key.length <= 8) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
4361
4670
  return key.slice(0, 4) + "\u2022\u2022\u2022\u2022" + key.slice(-4);
4362
4671
  }
4672
+ function appendToEnv(key, value) {
4673
+ const envPath = join11(getMercuryHome(), ".env");
4674
+ let envContent = "";
4675
+ if (existsSync16(envPath)) {
4676
+ envContent = readFileSync12(envPath, "utf-8");
4677
+ }
4678
+ const lines = envContent.split("\n").filter((l) => !l.startsWith(`${key}=`) && l.trim() !== "");
4679
+ lines.push(`${key}=${value}`);
4680
+ writeFileSync13(envPath, lines.join("\n") + "\n", "utf-8");
4681
+ process.env[key] = value;
4682
+ }
4363
4683
  async function configure(existingConfig) {
4364
4684
  const isReconfig = !!existingConfig;
4365
4685
  const config = existingConfig ?? loadConfig();
@@ -4458,6 +4778,23 @@ async function configure(existingConfig) {
4458
4778
  }
4459
4779
  hr();
4460
4780
  console.log("");
4781
+ console.log(chalk6.bold.white(" GitHub (optional)"));
4782
+ console.log(chalk6.dim(" Connect Mercury to GitHub for PRs, issues, and co-authored commits."));
4783
+ console.log(chalk6.dim(" Leave empty to skip. You can add it later with mercury doctor."));
4784
+ console.log("");
4785
+ const ghUserCurrent = isReconfig && config.github.username ? ` [${config.github.username}]` : "";
4786
+ const ghUsername = await ask(chalk6.white(` GitHub username${ghUserCurrent}: `));
4787
+ if (ghUsername) config.github.username = ghUsername;
4788
+ const ghEmailCurrent = isReconfig && config.github.email ? ` [${config.github.email}]` : "";
4789
+ const ghEmail = await ask(chalk6.white(` GitHub email${ghEmailCurrent}: `));
4790
+ if (ghEmail) config.github.email = ghEmail;
4791
+ const ghTokenCurrent = process.env.GITHUB_TOKEN ? ` [${maskKey(process.env.GITHUB_TOKEN)}]` : "";
4792
+ const ghToken = await ask(chalk6.white(` GitHub PAT (repo scope)${ghTokenCurrent}: `));
4793
+ if (ghToken) {
4794
+ appendToEnv("GITHUB_TOKEN", ghToken);
4795
+ }
4796
+ hr();
4797
+ console.log("");
4461
4798
  console.log(chalk6.bold.white(" Token Budget"));
4462
4799
  console.log("");
4463
4800
  const budgetPrompt = isReconfig ? chalk6.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `) : chalk6.white(` Daily token budget [${config.tokens.dailyBudget.toLocaleString()}]: `);
@@ -4558,6 +4895,9 @@ async function runAgent(isDaemon = false) {
4558
4895
  await msg.sendFile(filePath);
4559
4896
  }
4560
4897
  });
4898
+ if (process.env.GITHUB_TOKEN) {
4899
+ setGitHubToken(process.env.GITHUB_TOKEN);
4900
+ }
4561
4901
  capabilities.registerAll();
4562
4902
  const agent = new Agent(
4563
4903
  config,