@nairon-ai/aegis 0.2.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.
Files changed (167) hide show
  1. package/.agents/skills/bug-fix/SKILL.md +91 -0
  2. package/.flue/agents/bug-fix.ts +107 -0
  3. package/.flue/app.ts +16 -0
  4. package/Dockerfile +8 -0
  5. package/LICENSE +21 -0
  6. package/README.md +251 -0
  7. package/dist-node/agent/bug-fix-skill.d.ts +2 -0
  8. package/dist-node/agent/bug-fix-skill.d.ts.map +1 -0
  9. package/dist-node/agent/bug-fix-skill.js +64 -0
  10. package/dist-node/agent/bug-fix-skill.js.map +1 -0
  11. package/dist-node/agent/client.d.ts +14 -0
  12. package/dist-node/agent/client.d.ts.map +1 -0
  13. package/dist-node/agent/client.js +110 -0
  14. package/dist-node/agent/client.js.map +1 -0
  15. package/dist-node/cli/commands/deploy.d.ts +3 -0
  16. package/dist-node/cli/commands/deploy.d.ts.map +1 -0
  17. package/dist-node/cli/commands/deploy.js +94 -0
  18. package/dist-node/cli/commands/deploy.js.map +1 -0
  19. package/dist-node/cli/commands/init.d.ts +3 -0
  20. package/dist-node/cli/commands/init.d.ts.map +1 -0
  21. package/dist-node/cli/commands/init.js +115 -0
  22. package/dist-node/cli/commands/init.js.map +1 -0
  23. package/dist-node/cli/commands/pickup.d.ts +11 -0
  24. package/dist-node/cli/commands/pickup.d.ts.map +1 -0
  25. package/dist-node/cli/commands/pickup.js +43 -0
  26. package/dist-node/cli/commands/pickup.js.map +1 -0
  27. package/dist-node/cli/commands/setup.d.ts +3 -0
  28. package/dist-node/cli/commands/setup.d.ts.map +1 -0
  29. package/dist-node/cli/commands/setup.js +163 -0
  30. package/dist-node/cli/commands/setup.js.map +1 -0
  31. package/dist-node/cli/commands/status.d.ts +3 -0
  32. package/dist-node/cli/commands/status.d.ts.map +1 -0
  33. package/dist-node/cli/commands/status.js +26 -0
  34. package/dist-node/cli/commands/status.js.map +1 -0
  35. package/dist-node/cli/index.d.ts +3 -0
  36. package/dist-node/cli/index.d.ts.map +1 -0
  37. package/dist-node/cli/index.js +36 -0
  38. package/dist-node/cli/index.js.map +1 -0
  39. package/dist-node/cli/paths.d.ts +7 -0
  40. package/dist-node/cli/paths.d.ts.map +1 -0
  41. package/dist-node/cli/paths.js +29 -0
  42. package/dist-node/cli/paths.js.map +1 -0
  43. package/dist-node/cli/state.d.ts +16 -0
  44. package/dist-node/cli/state.d.ts.map +1 -0
  45. package/dist-node/cli/state.js +214 -0
  46. package/dist-node/cli/state.js.map +1 -0
  47. package/dist-node/core/pickup.d.ts +14 -0
  48. package/dist-node/core/pickup.d.ts.map +1 -0
  49. package/dist-node/core/pickup.js +42 -0
  50. package/dist-node/core/pickup.js.map +1 -0
  51. package/dist-node/github/index.d.ts +3 -0
  52. package/dist-node/github/index.d.ts.map +1 -0
  53. package/dist-node/github/index.js +2 -0
  54. package/dist-node/github/index.js.map +1 -0
  55. package/dist-node/github/manifest.d.ts +34 -0
  56. package/dist-node/github/manifest.d.ts.map +1 -0
  57. package/dist-node/github/manifest.js +71 -0
  58. package/dist-node/github/manifest.js.map +1 -0
  59. package/dist-node/integrations/github.d.ts +29 -0
  60. package/dist-node/integrations/github.d.ts.map +1 -0
  61. package/dist-node/integrations/github.js +199 -0
  62. package/dist-node/integrations/github.js.map +1 -0
  63. package/dist-node/integrations/linear.d.ts +15 -0
  64. package/dist-node/integrations/linear.d.ts.map +1 -0
  65. package/dist-node/integrations/linear.js +146 -0
  66. package/dist-node/integrations/linear.js.map +1 -0
  67. package/dist-node/integrations/telegram.d.ts +24 -0
  68. package/dist-node/integrations/telegram.d.ts.map +1 -0
  69. package/dist-node/integrations/telegram.js +39 -0
  70. package/dist-node/integrations/telegram.js.map +1 -0
  71. package/dist-node/integrations/webhooks.d.ts +3 -0
  72. package/dist-node/integrations/webhooks.d.ts.map +1 -0
  73. package/dist-node/integrations/webhooks.js +37 -0
  74. package/dist-node/integrations/webhooks.js.map +1 -0
  75. package/dist-node/sandbox/github-token.d.ts +7 -0
  76. package/dist-node/sandbox/github-token.d.ts.map +1 -0
  77. package/dist-node/sandbox/github-token.js +66 -0
  78. package/dist-node/sandbox/github-token.js.map +1 -0
  79. package/dist-node/sandbox/index.d.ts +2 -0
  80. package/dist-node/sandbox/index.d.ts.map +1 -0
  81. package/dist-node/sandbox/index.js +2 -0
  82. package/dist-node/sandbox/index.js.map +1 -0
  83. package/dist-node/server/app.d.ts +9 -0
  84. package/dist-node/server/app.d.ts.map +1 -0
  85. package/dist-node/server/app.js +216 -0
  86. package/dist-node/server/app.js.map +1 -0
  87. package/dist-node/shared/config.d.ts +5 -0
  88. package/dist-node/shared/config.d.ts.map +1 -0
  89. package/dist-node/shared/config.js +135 -0
  90. package/dist-node/shared/config.js.map +1 -0
  91. package/dist-node/shared/constants.d.ts +16 -0
  92. package/dist-node/shared/constants.d.ts.map +1 -0
  93. package/dist-node/shared/constants.js +29 -0
  94. package/dist-node/shared/constants.js.map +1 -0
  95. package/dist-node/shared/format.d.ts +12 -0
  96. package/dist-node/shared/format.d.ts.map +1 -0
  97. package/dist-node/shared/format.js +71 -0
  98. package/dist-node/shared/format.js.map +1 -0
  99. package/dist-node/shared/index.d.ts +7 -0
  100. package/dist-node/shared/index.d.ts.map +1 -0
  101. package/dist-node/shared/index.js +7 -0
  102. package/dist-node/shared/index.js.map +1 -0
  103. package/dist-node/shared/readiness.d.ts +3 -0
  104. package/dist-node/shared/readiness.d.ts.map +1 -0
  105. package/dist-node/shared/readiness.js +91 -0
  106. package/dist-node/shared/readiness.js.map +1 -0
  107. package/dist-node/shared/run-state.d.ts +5 -0
  108. package/dist-node/shared/run-state.d.ts.map +1 -0
  109. package/dist-node/shared/run-state.js +26 -0
  110. package/dist-node/shared/run-state.js.map +1 -0
  111. package/dist-node/shared/types.d.ts +230 -0
  112. package/dist-node/shared/types.d.ts.map +1 -0
  113. package/dist-node/shared/types.js +5 -0
  114. package/dist-node/shared/types.js.map +1 -0
  115. package/dist-node/sources/github.d.ts +15 -0
  116. package/dist-node/sources/github.d.ts.map +1 -0
  117. package/dist-node/sources/github.js +44 -0
  118. package/dist-node/sources/github.js.map +1 -0
  119. package/dist-node/sources/index.d.ts +6 -0
  120. package/dist-node/sources/index.d.ts.map +1 -0
  121. package/dist-node/sources/index.js +16 -0
  122. package/dist-node/sources/index.js.map +1 -0
  123. package/dist-node/sources/linear.d.ts +15 -0
  124. package/dist-node/sources/linear.d.ts.map +1 -0
  125. package/dist-node/sources/linear.js +32 -0
  126. package/dist-node/sources/linear.js.map +1 -0
  127. package/dist-node/sources/types.d.ts +15 -0
  128. package/dist-node/sources/types.d.ts.map +1 -0
  129. package/dist-node/sources/types.js +2 -0
  130. package/dist-node/sources/types.js.map +1 -0
  131. package/docs/RELEASING.md +52 -0
  132. package/docs/SETUP.md +439 -0
  133. package/package.json +64 -0
  134. package/src/agent/bug-fix-skill.ts +63 -0
  135. package/src/agent/client.ts +156 -0
  136. package/src/cli/commands/deploy.ts +106 -0
  137. package/src/cli/commands/init.ts +119 -0
  138. package/src/cli/commands/pickup.ts +44 -0
  139. package/src/cli/commands/setup.ts +217 -0
  140. package/src/cli/commands/status.ts +24 -0
  141. package/src/cli/index.ts +38 -0
  142. package/src/cli/paths.ts +29 -0
  143. package/src/cli/state.ts +228 -0
  144. package/src/core/pickup.ts +66 -0
  145. package/src/github/index.ts +2 -0
  146. package/src/github/manifest.ts +97 -0
  147. package/src/integrations/github.ts +241 -0
  148. package/src/integrations/linear.ts +195 -0
  149. package/src/integrations/telegram.ts +48 -0
  150. package/src/integrations/webhooks.ts +53 -0
  151. package/src/sandbox/github-token.ts +92 -0
  152. package/src/sandbox/index.ts +1 -0
  153. package/src/server/app.ts +292 -0
  154. package/src/shared/config.ts +154 -0
  155. package/src/shared/constants.ts +30 -0
  156. package/src/shared/format.ts +84 -0
  157. package/src/shared/index.ts +6 -0
  158. package/src/shared/readiness.ts +116 -0
  159. package/src/shared/run-state.ts +32 -0
  160. package/src/shared/types.ts +257 -0
  161. package/src/sources/github.ts +57 -0
  162. package/src/sources/index.ts +20 -0
  163. package/src/sources/linear.ts +44 -0
  164. package/src/sources/types.ts +16 -0
  165. package/tsconfig.json +25 -0
  166. package/tsconfig.node.json +16 -0
  167. package/wrangler.jsonc +43 -0
@@ -0,0 +1,106 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ import { defineCommand } from "citty";
4
+ import { consola } from "consola";
5
+ import { findPackageRoot } from "../paths.js";
6
+ import { loadConfig, saveConfig } from "../state.js";
7
+
8
+ export const deployCommand = defineCommand({
9
+ meta: { name: "deploy", description: "Build and deploy the Aegis Worker" },
10
+ async run() {
11
+ await runDeploy();
12
+ },
13
+ });
14
+
15
+ export async function runDeploy(): Promise<void> {
16
+ const packageRoot = findPackageRoot(import.meta.url);
17
+ const config = loadConfig();
18
+ const flueBin = resolve(packageRoot, "node_modules/.bin/flue");
19
+ const wranglerBin = resolve(packageRoot, "node_modules/.bin/wrangler");
20
+
21
+ consola.start("Building Flue Worker...");
22
+ execFileSync(flueBin, ["build", "--target", "cloudflare"], {
23
+ cwd: packageRoot,
24
+ stdio: "inherit",
25
+ });
26
+
27
+ pushSecrets(toSecrets(config), packageRoot, wranglerBin);
28
+
29
+ consola.start("Deploying with Wrangler...");
30
+ const output = execFileSync(
31
+ wranglerBin,
32
+ ["deploy", "--config", resolve(packageRoot, "dist/wrangler.jsonc")],
33
+ {
34
+ cwd: packageRoot,
35
+ stdio: "pipe",
36
+ encoding: "utf-8",
37
+ },
38
+ );
39
+ consola.log(output);
40
+ const match = output.match(/https:\/\/[^\s]+\.workers\.dev/);
41
+ if (match) {
42
+ config.workerUrl = match[0];
43
+ saveConfig(config);
44
+ pushSecrets({ AEGIS_WORKER_URL: match[0] }, packageRoot, wranglerBin);
45
+ consola.success(`Saved Worker URL: ${match[0]}`);
46
+ } else {
47
+ consola.success("Deploy complete.");
48
+ }
49
+ }
50
+
51
+ function pushSecrets(
52
+ secrets: Record<string, string | undefined>,
53
+ packageRoot: string,
54
+ wranglerBin: string,
55
+ ): void {
56
+ consola.start("Pushing Cloudflare secrets...");
57
+ const wranglerConfig = resolve(packageRoot, "wrangler.jsonc");
58
+ for (const [key, value] of Object.entries(secrets)) {
59
+ if (!value) continue;
60
+ const result = spawnSync(wranglerBin, ["secret", "put", key, "--config", wranglerConfig], {
61
+ cwd: packageRoot,
62
+ input: value,
63
+ stdio: ["pipe", "pipe", "pipe"],
64
+ encoding: "utf-8",
65
+ });
66
+ if (result.status !== 0) {
67
+ throw new Error(`Failed to set ${key}: ${result.stderr || result.stdout}`);
68
+ }
69
+ }
70
+ consola.success("Secrets pushed");
71
+ }
72
+
73
+ function toSecrets(config: ReturnType<typeof loadConfig>): Record<string, string | undefined> {
74
+ return {
75
+ AEGIS_WORKER_URL: config.workerUrl,
76
+ MONITORED_REPO: config.monitoredRepo,
77
+ BASE_BRANCH: config.baseBranch,
78
+ AUTOMATION_MODE: config.automationMode,
79
+ CONTEXT_PROFILE: config.contextProfile,
80
+ READY_LABEL: config.readyLabel,
81
+ BUG_LABEL: config.bugLabel,
82
+ GITHUB_APP_ID: config.githubAppId,
83
+ GITHUB_APP_PRIVATE_KEY: config.githubAppPrivateKey,
84
+ GITHUB_INSTALLATION_ID: config.githubInstallationId,
85
+ GITHUB_WEBHOOK_SECRET: config.githubWebhookSecret,
86
+ GITHUB_TOKEN: config.githubToken,
87
+ LINEAR_API_KEY: config.linearApiKey,
88
+ LINEAR_WEBHOOK_SECRET: config.linearWebhookSecret,
89
+ LINEAR_TEAM_ID: config.linearTeamId,
90
+ LINEAR_PROJECT_ID: config.linearProjectId,
91
+ LINEAR_READY_STATUS: config.linearReadyStatus,
92
+ LINEAR_IN_PROGRESS_STATUS: config.linearInProgressStatus,
93
+ LINEAR_NEEDS_INFO_STATUS: config.linearNeedsInfoStatus,
94
+ LINEAR_BLOCKED_STATUS: config.linearBlockedStatus,
95
+ LINEAR_BUG_LABEL: config.linearBugLabel,
96
+ TELEGRAM_BOT_TOKEN: config.telegramBotToken,
97
+ TELEGRAM_CHAT_ID: config.telegramChatId,
98
+ TELEGRAM_WEBHOOK_SECRET: config.telegramWebhookSecret,
99
+ DATABASE_URL: config.databaseUrl,
100
+ VERCEL_TOKEN: config.vercelToken,
101
+ VERCEL_PROJECT_ID: config.vercelProjectId,
102
+ VERCEL_TEAM_ID: config.vercelTeamId,
103
+ AGENT_MODEL: config.agentModel,
104
+ OPENAI_API_KEY: config.openaiApiKey,
105
+ };
106
+ }
@@ -0,0 +1,119 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { defineCommand } from "citty";
4
+ import { consola } from "consola";
5
+ import { PROJECT_AEGIS_DIR } from "../paths.js";
6
+
7
+ const ENV_EXAMPLE = `# Required
8
+ MONITORED_REPO=KeyLead-Team/keylead
9
+ BASE_BRANCH=main
10
+ AGENT_MODEL=openai/gpt-5.1
11
+ OPENAI_API_KEY=sk-...
12
+
13
+ # Labels / automation
14
+ READY_LABEL=ready to implement
15
+ BUG_LABEL=bug
16
+ AUTOMATION_MODE=plan-first
17
+ CONTEXT_PROFILE=minimal
18
+ MAX_CONCURRENT_RUNS=1
19
+
20
+ # GitHub auth: use either GitHub App fields or GITHUB_TOKEN
21
+ GITHUB_APP_ID=
22
+ GITHUB_APP_PRIVATE_KEY=
23
+ GITHUB_INSTALLATION_ID=
24
+ GITHUB_WEBHOOK_SECRET=
25
+ GITHUB_TOKEN=
26
+
27
+ # Linear optional
28
+ LINEAR_API_KEY=
29
+ LINEAR_WEBHOOK_SECRET=
30
+ LINEAR_TEAM_ID=
31
+ LINEAR_PROJECT_ID=
32
+ LINEAR_READY_STATUS=Ready to Implement
33
+ LINEAR_IN_PROGRESS_STATUS=In Progress
34
+ LINEAR_NEEDS_INFO_STATUS=Needs Info
35
+ LINEAR_BLOCKED_STATUS=Blocked
36
+ LINEAR_BUG_LABEL=bug
37
+
38
+ # Telegram optional
39
+ TELEGRAM_BOT_TOKEN=
40
+ TELEGRAM_CHAT_ID=
41
+ TELEGRAM_WEBHOOK_SECRET=
42
+
43
+ # Production context optional. Prefer read-only DB access.
44
+ DATABASE_URL=
45
+ VERCEL_TOKEN=
46
+ VERCEL_PROJECT_ID=
47
+ VERCEL_TEAM_ID=
48
+
49
+ # Filled by aegis deploy
50
+ AEGIS_WORKER_URL=
51
+ `;
52
+
53
+ const README = `# Aegis
54
+
55
+ This directory configures Aegis for this repository.
56
+
57
+ ## First Run
58
+
59
+ \`\`\`bash
60
+ npx @nairon-ai/aegis setup
61
+ npx @nairon-ai/aegis pickup --dry-run
62
+ npx @nairon-ai/aegis deploy
63
+ \`\`\`
64
+
65
+ For the first test, use:
66
+
67
+ - \`AUTOMATION_MODE=plan-first\`
68
+ - \`CONTEXT_PROFILE=minimal\`
69
+ - \`MAX_CONCURRENT_RUNS=1\`
70
+
71
+ Create these labels in GitHub:
72
+
73
+ - \`bug\`
74
+ - \`ready to implement\`
75
+
76
+ Then create a tiny test bug issue with both labels.
77
+
78
+ ## Webhook URLs
79
+
80
+ After deploy, add these webhooks where needed:
81
+
82
+ \`\`\`text
83
+ GitHub: https://YOUR_WORKER.workers.dev/webhook/github
84
+ Linear: https://YOUR_WORKER.workers.dev/webhook/linear
85
+ Telegram: https://YOUR_WORKER.workers.dev/webhook/telegram
86
+ \`\`\`
87
+
88
+ Keep \`.aegis/.env\` out of git.
89
+ `;
90
+
91
+ export const initCommand = defineCommand({
92
+ meta: { name: "init", description: "Scaffold .aegis config in this repo" },
93
+ async run() {
94
+ runInit();
95
+ },
96
+ });
97
+
98
+ export function runInit(): void {
99
+ mkdirSync(PROJECT_AEGIS_DIR, { recursive: true });
100
+ writeIfMissing(resolve(PROJECT_AEGIS_DIR, ".gitignore"), ".env\nlogs/\n");
101
+ writeIfMissing(resolve(PROJECT_AEGIS_DIR, ".env.example"), ENV_EXAMPLE);
102
+ writeIfMissing(resolve(PROJECT_AEGIS_DIR, "README.md"), README);
103
+ consola.success("Created .aegis/");
104
+ consola.log("\nNext:");
105
+ consola.log(" 1. npx @nairon-ai/aegis setup");
106
+ consola.log(" 2. create GitHub labels: bug, ready to implement");
107
+ consola.log(" 3. create a tiny test bug issue");
108
+ consola.log(" 4. npx @nairon-ai/aegis pickup --dry-run");
109
+ consola.log(" 5. npx @nairon-ai/aegis deploy");
110
+ consola.log("");
111
+ }
112
+
113
+ function writeIfMissing(path: string, contents: string): void {
114
+ if (existsSync(path)) {
115
+ consola.info(`Skipped existing ${path}`);
116
+ return;
117
+ }
118
+ writeFileSync(path, contents);
119
+ }
@@ -0,0 +1,44 @@
1
+ import { defineCommand } from "citty";
2
+ import { consola } from "consola";
3
+ import { scanReadyBugs } from "../../core/pickup.js";
4
+ import { configFromCli } from "../../shared/config.js";
5
+ import { formatReadyDecisionLine } from "../../shared/format.js";
6
+ import { loadConfig } from "../state.js";
7
+
8
+ export const pickupCommand = defineCommand({
9
+ meta: {
10
+ name: "pickup",
11
+ description: "Find GitHub/Linear bugs Aegis would pick up",
12
+ },
13
+ args: {
14
+ "dry-run": {
15
+ type: "boolean",
16
+ description: "Only report decisions; do not claim",
17
+ default: true,
18
+ },
19
+ },
20
+ async run({ args }) {
21
+ await runPickup({ dryRun: args.dryRun !== false });
22
+ },
23
+ });
24
+
25
+ export async function runPickup(_options: { dryRun: boolean }): Promise<void> {
26
+ const config = configFromCli(loadConfig());
27
+ if (!config.monitoredRepo) {
28
+ consola.error("MONITORED_REPO is missing. Run `aegis setup` first.");
29
+ return;
30
+ }
31
+ consola.start("Scanning ready bugs...\n");
32
+ const decisions = await scanReadyBugs(config);
33
+ if (!decisions.length) {
34
+ consola.info("No ready bugs found.");
35
+ return;
36
+ }
37
+ for (const decision of decisions) {
38
+ consola.log(` ${formatReadyDecisionLine(decision.item, decision.decision)}`);
39
+ consola.log(` ${decision.item.title}`);
40
+ consola.log(` ${decision.item.url}`);
41
+ }
42
+ consola.log("");
43
+ consola.info("Dry run only. Deployed worker claims bugs on schedule or via POST /api/pickup.");
44
+ }
@@ -0,0 +1,217 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { defineCommand } from "citty";
4
+ import { consola } from "consola";
5
+ import type { AegisCliConfig } from "../../shared/types.js";
6
+ import { resolveEnvFileForWrite } from "../paths.js";
7
+ import { detectSetupState, printState, saveConfig } from "../state.js";
8
+
9
+ export const setupCommand = defineCommand({
10
+ meta: { name: "setup", description: "Configure Aegis for a repo" },
11
+ async run() {
12
+ await runSetupWizard();
13
+ },
14
+ });
15
+
16
+ export async function runSetupWizard(): Promise<void> {
17
+ consola.log("\n Aegis - AFK bug-fixing agent\n");
18
+ const state = detectSetupState();
19
+ printState(state);
20
+ const config = state.config;
21
+
22
+ await setupRepo(config);
23
+ await setupGitHub(config);
24
+ await setupLinear(config);
25
+ await setupAutomation(config);
26
+ await setupProductionContext(config);
27
+ await setupTelegram(config);
28
+ await setupWorker(config);
29
+
30
+ saveConfig(config);
31
+ consola.success(`Saved ${resolveEnvFileForWrite()}`);
32
+ consola.log("\nNext:");
33
+ consola.log(" 1. Create labels in the monitored repo: bug, ready to implement");
34
+ consola.log(" 2. Create a test GitHub issue with both labels");
35
+ consola.log(" 3. npx @nairon-ai/aegis pickup --dry-run");
36
+ consola.log(" 4. npx @nairon-ai/aegis deploy");
37
+ consola.log(" 5. Add GitHub/Linear/Telegram webhook URLs after deploy");
38
+ consola.log("");
39
+ }
40
+
41
+ async function setupRepo(config: AegisCliConfig): Promise<void> {
42
+ config.monitoredRepo = await promptText(
43
+ "GitHub repo to monitor (owner/repo):",
44
+ config.monitoredRepo,
45
+ );
46
+ config.baseBranch = await promptText("Base branch:", config.baseBranch ?? "main");
47
+ }
48
+
49
+ async function setupGitHub(config: AegisCliConfig): Promise<void> {
50
+ consola.log("\nGitHub access");
51
+ consola.info(
52
+ "For the first test, a fine-grained token is fastest. For shared/team use, use a GitHub App.",
53
+ );
54
+ const method = (await consola.prompt("Auth method:", {
55
+ type: "select",
56
+ options: [
57
+ { label: "GitHub App (recommended)", value: "app" },
58
+ { label: "GitHub token (simpler local testing)", value: "token" },
59
+ ],
60
+ })) as string;
61
+
62
+ if (method === "token") {
63
+ config.githubToken = await promptText(
64
+ "GitHub token with contents/issues/PR read-write access:",
65
+ config.githubToken,
66
+ );
67
+ } else {
68
+ config.githubAppId = await promptText("GitHub App ID:", config.githubAppId);
69
+ const keyPath = await promptText("Path to GitHub App private key .pem:", undefined);
70
+ if (keyPath) {
71
+ const resolved = resolve(keyPath.replace(/^~/, process.env.HOME ?? "~"));
72
+ if (existsSync(resolved)) {
73
+ config.githubAppPrivateKey = Buffer.from(readFileSync(resolved, "utf-8")).toString(
74
+ "base64",
75
+ );
76
+ consola.success("Loaded private key");
77
+ } else {
78
+ consola.warn(`File not found: ${resolved}`);
79
+ }
80
+ }
81
+ if (!config.githubAppPrivateKey) {
82
+ config.githubAppPrivateKey = Buffer.from(
83
+ await promptText("Paste GitHub App private key PEM:", config.githubAppPrivateKey),
84
+ ).toString("base64");
85
+ }
86
+ config.githubInstallationId = await promptText(
87
+ "GitHub App installation ID:",
88
+ config.githubInstallationId,
89
+ );
90
+ }
91
+
92
+ const generateSecret = (await consola.prompt("Generate a GitHub webhook secret?", {
93
+ type: "confirm",
94
+ initial: !config.githubWebhookSecret,
95
+ })) as boolean;
96
+ config.githubWebhookSecret = generateSecret
97
+ ? crypto.randomUUID()
98
+ : await promptText("GitHub webhook secret:", config.githubWebhookSecret);
99
+ if (config.githubWebhookSecret) {
100
+ consola.info("Use this same value when you add the GitHub App or repo webhook.");
101
+ }
102
+ }
103
+
104
+ async function setupLinear(config: AegisCliConfig): Promise<void> {
105
+ const enable = (await consola.prompt("Connect Linear too?", {
106
+ type: "confirm",
107
+ initial: Boolean(config.linearApiKey),
108
+ })) as boolean;
109
+ if (!enable) return;
110
+
111
+ config.linearApiKey = await promptText("Linear API key:", config.linearApiKey);
112
+ const generateSecret = (await consola.prompt("Generate a Linear webhook signing secret?", {
113
+ type: "confirm",
114
+ initial: !config.linearWebhookSecret,
115
+ })) as boolean;
116
+ config.linearWebhookSecret = generateSecret
117
+ ? crypto.randomUUID()
118
+ : await promptText("Linear webhook signing secret:", config.linearWebhookSecret);
119
+ config.linearTeamId = await promptText("Linear team ID:", config.linearTeamId);
120
+ config.linearProjectId = await promptText(
121
+ "Linear project ID (optional):",
122
+ config.linearProjectId,
123
+ );
124
+ config.linearReadyStatus = await promptText(
125
+ "Linear ready status name:",
126
+ config.linearReadyStatus ?? "Ready to Implement",
127
+ );
128
+ config.linearInProgressStatus = await promptText(
129
+ "Linear in-progress status name:",
130
+ config.linearInProgressStatus ?? "In Progress",
131
+ );
132
+ config.linearNeedsInfoStatus = await promptText(
133
+ "Linear needs-info status name:",
134
+ config.linearNeedsInfoStatus ?? "Needs Info",
135
+ );
136
+ config.linearBlockedStatus = await promptText(
137
+ "Linear blocked status name:",
138
+ config.linearBlockedStatus ?? "Blocked",
139
+ );
140
+ config.linearBugLabel = await promptText("Linear bug label:", config.linearBugLabel ?? "bug");
141
+ }
142
+
143
+ async function setupAutomation(config: AegisCliConfig): Promise<void> {
144
+ config.readyLabel = await promptText(
145
+ "GitHub ready label:",
146
+ config.readyLabel ?? "ready to implement",
147
+ );
148
+ config.bugLabel = await promptText("GitHub bug label:", config.bugLabel ?? "bug");
149
+ config.automationMode = (await consola.prompt("Automation mode:", {
150
+ type: "select",
151
+ options: [
152
+ { label: "Plan first, ask approval before patching", value: "plan-first" },
153
+ { label: "Auto-implement only bugs labeled low-risk", value: "auto-low-risk" },
154
+ ],
155
+ initial: config.automationMode ?? "plan-first",
156
+ })) as AegisCliConfig["automationMode"];
157
+ config.agentModel = await promptText("Flue model:", config.agentModel ?? "openai/gpt-5.1");
158
+ config.openaiApiKey = await promptText(
159
+ "OpenAI API key (if using an OpenAI model):",
160
+ config.openaiApiKey,
161
+ );
162
+ }
163
+
164
+ async function setupProductionContext(config: AegisCliConfig): Promise<void> {
165
+ const profile = (await consola.prompt("Context profile:", {
166
+ type: "select",
167
+ options: [
168
+ { label: "Minimal: issue + repo", value: "minimal" },
169
+ { label: "Production: add read-only DB and Vercel logs", value: "production" },
170
+ ],
171
+ initial: config.contextProfile ?? "minimal",
172
+ })) as AegisCliConfig["contextProfile"];
173
+ config.contextProfile = profile;
174
+ if (profile !== "production") return;
175
+
176
+ consola.warn("Use a read-only DB user or replica. Do not give Aegis write access.");
177
+ config.databaseUrl = await promptText("Read-only DATABASE_URL (optional):", config.databaseUrl);
178
+ config.vercelToken = await promptText("Vercel API token (optional):", config.vercelToken);
179
+ config.vercelProjectId = await promptText(
180
+ "Vercel project ID (optional):",
181
+ config.vercelProjectId,
182
+ );
183
+ config.vercelTeamId = await promptText("Vercel team ID (optional):", config.vercelTeamId);
184
+ }
185
+
186
+ async function setupTelegram(config: AegisCliConfig): Promise<void> {
187
+ const enable = (await consola.prompt("Enable Telegram approval pings?", {
188
+ type: "confirm",
189
+ initial: Boolean(config.telegramBotToken),
190
+ })) as boolean;
191
+ if (!enable) return;
192
+
193
+ config.telegramBotToken = await promptText("Telegram bot token:", config.telegramBotToken);
194
+ config.telegramChatId = await promptText("Telegram chat ID:", config.telegramChatId);
195
+ const generateSecret = (await consola.prompt("Generate a Telegram webhook secret?", {
196
+ type: "confirm",
197
+ initial: !config.telegramWebhookSecret,
198
+ })) as boolean;
199
+ config.telegramWebhookSecret = generateSecret
200
+ ? crypto.randomUUID()
201
+ : await promptText("Telegram webhook secret:", config.telegramWebhookSecret);
202
+ }
203
+
204
+ async function setupWorker(config: AegisCliConfig): Promise<void> {
205
+ config.workerUrl = await promptText(
206
+ "Deployed Worker URL (can fill after first deploy):",
207
+ config.workerUrl,
208
+ );
209
+ }
210
+
211
+ async function promptText(message: string, initial: string | undefined): Promise<string> {
212
+ const value = (await consola.prompt(message, {
213
+ type: "text",
214
+ initial,
215
+ })) as string;
216
+ return value?.trim() ?? "";
217
+ }
@@ -0,0 +1,24 @@
1
+ import { defineCommand } from "citty";
2
+ import { consola } from "consola";
3
+ import { detectSetupState, printState } from "../state.js";
4
+
5
+ export const statusCommand = defineCommand({
6
+ meta: { name: "status", description: "Show Aegis setup status" },
7
+ async run() {
8
+ await runStatus();
9
+ },
10
+ });
11
+
12
+ export async function runStatus(): Promise<void> {
13
+ const state = detectSetupState();
14
+ consola.log("\n Aegis Status");
15
+ printState(state);
16
+ if (state.config.monitoredRepo) consola.log(` Repo: ${state.config.monitoredRepo}`);
17
+ if (state.config.workerUrl) consola.log(` Worker: ${state.config.workerUrl}`);
18
+ consola.log("");
19
+ if (state.isUsable) {
20
+ consola.success("Aegis can scan GitHub issues.");
21
+ } else {
22
+ consola.warn("Run `aegis setup` before pickup/deploy.");
23
+ }
24
+ }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { defineCommand, runMain } from "citty";
4
+ import { deployCommand } from "./commands/deploy.js";
5
+ import { initCommand } from "./commands/init.js";
6
+ import { pickupCommand } from "./commands/pickup.js";
7
+ import { setupCommand } from "./commands/setup.js";
8
+ import { statusCommand } from "./commands/status.js";
9
+
10
+ const main = defineCommand({
11
+ meta: {
12
+ name: "aegis",
13
+ version: "0.2.0",
14
+ description: "Aegis - self-hosted AFK bug-fixing agent",
15
+ },
16
+ default: "menu",
17
+ subCommands: {
18
+ init: initCommand,
19
+ setup: setupCommand,
20
+ pickup: pickupCommand,
21
+ status: statusCommand,
22
+ deploy: deployCommand,
23
+ menu: defineCommand({
24
+ meta: { name: "menu", description: "Open the interactive menu", hidden: true },
25
+ async run() {
26
+ const { detectSetupState, runInteractiveMenu } = await import("./state.js");
27
+ if (!detectSetupState().isUsable) {
28
+ const { runSetupWizard } = await import("./commands/setup.js");
29
+ await runSetupWizard();
30
+ return;
31
+ }
32
+ await runInteractiveMenu();
33
+ },
34
+ }),
35
+ },
36
+ });
37
+
38
+ runMain(main);
@@ -0,0 +1,29 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export const PROJECT_AEGIS_DIR = resolve(process.cwd(), ".aegis");
6
+ export const PROJECT_AEGIS_ENV_FILE = resolve(PROJECT_AEGIS_DIR, ".env");
7
+ export const LEGACY_ENV_FILE = resolve(process.cwd(), ".env.local");
8
+
9
+ export function resolveEnvFileForRead(): string | undefined {
10
+ if (existsSync(PROJECT_AEGIS_ENV_FILE)) return PROJECT_AEGIS_ENV_FILE;
11
+ if (existsSync(LEGACY_ENV_FILE)) return LEGACY_ENV_FILE;
12
+ return undefined;
13
+ }
14
+
15
+ export function resolveEnvFileForWrite(): string {
16
+ if (existsSync(PROJECT_AEGIS_DIR)) return PROJECT_AEGIS_ENV_FILE;
17
+ return LEGACY_ENV_FILE;
18
+ }
19
+
20
+ export function findPackageRoot(fromUrl: string): string {
21
+ let current = dirname(fileURLToPath(fromUrl));
22
+ while (current !== dirname(current)) {
23
+ if (existsSync(resolve(current, "package.json")) && existsSync(resolve(current, ".flue"))) {
24
+ return current;
25
+ }
26
+ current = dirname(current);
27
+ }
28
+ throw new Error("Could not find the installed Aegis package root");
29
+ }