@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.
- package/.agents/skills/bug-fix/SKILL.md +91 -0
- package/.flue/agents/bug-fix.ts +107 -0
- package/.flue/app.ts +16 -0
- package/Dockerfile +8 -0
- package/LICENSE +21 -0
- package/README.md +251 -0
- package/dist-node/agent/bug-fix-skill.d.ts +2 -0
- package/dist-node/agent/bug-fix-skill.d.ts.map +1 -0
- package/dist-node/agent/bug-fix-skill.js +64 -0
- package/dist-node/agent/bug-fix-skill.js.map +1 -0
- package/dist-node/agent/client.d.ts +14 -0
- package/dist-node/agent/client.d.ts.map +1 -0
- package/dist-node/agent/client.js +110 -0
- package/dist-node/agent/client.js.map +1 -0
- package/dist-node/cli/commands/deploy.d.ts +3 -0
- package/dist-node/cli/commands/deploy.d.ts.map +1 -0
- package/dist-node/cli/commands/deploy.js +94 -0
- package/dist-node/cli/commands/deploy.js.map +1 -0
- package/dist-node/cli/commands/init.d.ts +3 -0
- package/dist-node/cli/commands/init.d.ts.map +1 -0
- package/dist-node/cli/commands/init.js +115 -0
- package/dist-node/cli/commands/init.js.map +1 -0
- package/dist-node/cli/commands/pickup.d.ts +11 -0
- package/dist-node/cli/commands/pickup.d.ts.map +1 -0
- package/dist-node/cli/commands/pickup.js +43 -0
- package/dist-node/cli/commands/pickup.js.map +1 -0
- package/dist-node/cli/commands/setup.d.ts +3 -0
- package/dist-node/cli/commands/setup.d.ts.map +1 -0
- package/dist-node/cli/commands/setup.js +163 -0
- package/dist-node/cli/commands/setup.js.map +1 -0
- package/dist-node/cli/commands/status.d.ts +3 -0
- package/dist-node/cli/commands/status.d.ts.map +1 -0
- package/dist-node/cli/commands/status.js +26 -0
- package/dist-node/cli/commands/status.js.map +1 -0
- package/dist-node/cli/index.d.ts +3 -0
- package/dist-node/cli/index.d.ts.map +1 -0
- package/dist-node/cli/index.js +36 -0
- package/dist-node/cli/index.js.map +1 -0
- package/dist-node/cli/paths.d.ts +7 -0
- package/dist-node/cli/paths.d.ts.map +1 -0
- package/dist-node/cli/paths.js +29 -0
- package/dist-node/cli/paths.js.map +1 -0
- package/dist-node/cli/state.d.ts +16 -0
- package/dist-node/cli/state.d.ts.map +1 -0
- package/dist-node/cli/state.js +214 -0
- package/dist-node/cli/state.js.map +1 -0
- package/dist-node/core/pickup.d.ts +14 -0
- package/dist-node/core/pickup.d.ts.map +1 -0
- package/dist-node/core/pickup.js +42 -0
- package/dist-node/core/pickup.js.map +1 -0
- package/dist-node/github/index.d.ts +3 -0
- package/dist-node/github/index.d.ts.map +1 -0
- package/dist-node/github/index.js +2 -0
- package/dist-node/github/index.js.map +1 -0
- package/dist-node/github/manifest.d.ts +34 -0
- package/dist-node/github/manifest.d.ts.map +1 -0
- package/dist-node/github/manifest.js +71 -0
- package/dist-node/github/manifest.js.map +1 -0
- package/dist-node/integrations/github.d.ts +29 -0
- package/dist-node/integrations/github.d.ts.map +1 -0
- package/dist-node/integrations/github.js +199 -0
- package/dist-node/integrations/github.js.map +1 -0
- package/dist-node/integrations/linear.d.ts +15 -0
- package/dist-node/integrations/linear.d.ts.map +1 -0
- package/dist-node/integrations/linear.js +146 -0
- package/dist-node/integrations/linear.js.map +1 -0
- package/dist-node/integrations/telegram.d.ts +24 -0
- package/dist-node/integrations/telegram.d.ts.map +1 -0
- package/dist-node/integrations/telegram.js +39 -0
- package/dist-node/integrations/telegram.js.map +1 -0
- package/dist-node/integrations/webhooks.d.ts +3 -0
- package/dist-node/integrations/webhooks.d.ts.map +1 -0
- package/dist-node/integrations/webhooks.js +37 -0
- package/dist-node/integrations/webhooks.js.map +1 -0
- package/dist-node/sandbox/github-token.d.ts +7 -0
- package/dist-node/sandbox/github-token.d.ts.map +1 -0
- package/dist-node/sandbox/github-token.js +66 -0
- package/dist-node/sandbox/github-token.js.map +1 -0
- package/dist-node/sandbox/index.d.ts +2 -0
- package/dist-node/sandbox/index.d.ts.map +1 -0
- package/dist-node/sandbox/index.js +2 -0
- package/dist-node/sandbox/index.js.map +1 -0
- package/dist-node/server/app.d.ts +9 -0
- package/dist-node/server/app.d.ts.map +1 -0
- package/dist-node/server/app.js +216 -0
- package/dist-node/server/app.js.map +1 -0
- package/dist-node/shared/config.d.ts +5 -0
- package/dist-node/shared/config.d.ts.map +1 -0
- package/dist-node/shared/config.js +135 -0
- package/dist-node/shared/config.js.map +1 -0
- package/dist-node/shared/constants.d.ts +16 -0
- package/dist-node/shared/constants.d.ts.map +1 -0
- package/dist-node/shared/constants.js +29 -0
- package/dist-node/shared/constants.js.map +1 -0
- package/dist-node/shared/format.d.ts +12 -0
- package/dist-node/shared/format.d.ts.map +1 -0
- package/dist-node/shared/format.js +71 -0
- package/dist-node/shared/format.js.map +1 -0
- package/dist-node/shared/index.d.ts +7 -0
- package/dist-node/shared/index.d.ts.map +1 -0
- package/dist-node/shared/index.js +7 -0
- package/dist-node/shared/index.js.map +1 -0
- package/dist-node/shared/readiness.d.ts +3 -0
- package/dist-node/shared/readiness.d.ts.map +1 -0
- package/dist-node/shared/readiness.js +91 -0
- package/dist-node/shared/readiness.js.map +1 -0
- package/dist-node/shared/run-state.d.ts +5 -0
- package/dist-node/shared/run-state.d.ts.map +1 -0
- package/dist-node/shared/run-state.js +26 -0
- package/dist-node/shared/run-state.js.map +1 -0
- package/dist-node/shared/types.d.ts +230 -0
- package/dist-node/shared/types.d.ts.map +1 -0
- package/dist-node/shared/types.js +5 -0
- package/dist-node/shared/types.js.map +1 -0
- package/dist-node/sources/github.d.ts +15 -0
- package/dist-node/sources/github.d.ts.map +1 -0
- package/dist-node/sources/github.js +44 -0
- package/dist-node/sources/github.js.map +1 -0
- package/dist-node/sources/index.d.ts +6 -0
- package/dist-node/sources/index.d.ts.map +1 -0
- package/dist-node/sources/index.js +16 -0
- package/dist-node/sources/index.js.map +1 -0
- package/dist-node/sources/linear.d.ts +15 -0
- package/dist-node/sources/linear.d.ts.map +1 -0
- package/dist-node/sources/linear.js +32 -0
- package/dist-node/sources/linear.js.map +1 -0
- package/dist-node/sources/types.d.ts +15 -0
- package/dist-node/sources/types.d.ts.map +1 -0
- package/dist-node/sources/types.js +2 -0
- package/dist-node/sources/types.js.map +1 -0
- package/docs/RELEASING.md +52 -0
- package/docs/SETUP.md +439 -0
- package/package.json +64 -0
- package/src/agent/bug-fix-skill.ts +63 -0
- package/src/agent/client.ts +156 -0
- package/src/cli/commands/deploy.ts +106 -0
- package/src/cli/commands/init.ts +119 -0
- package/src/cli/commands/pickup.ts +44 -0
- package/src/cli/commands/setup.ts +217 -0
- package/src/cli/commands/status.ts +24 -0
- package/src/cli/index.ts +38 -0
- package/src/cli/paths.ts +29 -0
- package/src/cli/state.ts +228 -0
- package/src/core/pickup.ts +66 -0
- package/src/github/index.ts +2 -0
- package/src/github/manifest.ts +97 -0
- package/src/integrations/github.ts +241 -0
- package/src/integrations/linear.ts +195 -0
- package/src/integrations/telegram.ts +48 -0
- package/src/integrations/webhooks.ts +53 -0
- package/src/sandbox/github-token.ts +92 -0
- package/src/sandbox/index.ts +1 -0
- package/src/server/app.ts +292 -0
- package/src/shared/config.ts +154 -0
- package/src/shared/constants.ts +30 -0
- package/src/shared/format.ts +84 -0
- package/src/shared/index.ts +6 -0
- package/src/shared/readiness.ts +116 -0
- package/src/shared/run-state.ts +32 -0
- package/src/shared/types.ts +257 -0
- package/src/sources/github.ts +57 -0
- package/src/sources/index.ts +20 -0
- package/src/sources/linear.ts +44 -0
- package/src/sources/types.ts +16 -0
- package/tsconfig.json +25 -0
- package/tsconfig.node.json +16 -0
- 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
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -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);
|
package/src/cli/paths.ts
ADDED
|
@@ -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
|
+
}
|