@elmiristic/agent-ready 0.1.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/README.md +41 -0
- package/bin/agent-ready.js +19 -0
- package/package.json +36 -0
- package/src/init.js +28 -0
- package/src/steps/collectAnthropic.js +23 -0
- package/src/steps/collectTelegram.js +25 -0
- package/src/steps/collectTrello.js +76 -0
- package/src/steps/done.js +29 -0
- package/src/steps/scaffold.js +48 -0
- package/src/steps/setGithubSecrets.js +60 -0
- package/src/steps/welcome.js +22 -0
- package/src/templates/agentScript.js +225 -0
- package/src/templates/doneWorkflow.js +38 -0
- package/src/templates/workflow.js +42 -0
- package/telegram-bot/api/send.js +38 -0
- package/telegram-bot/api/telegram.js +43 -0
- package/telegram-bot/package.json +13 -0
- package/telegram-bot/vercel.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# agent-ready
|
|
2
|
+
|
|
3
|
+
Wire an AI coding agent into any GitHub repo in 5 minutes.
|
|
4
|
+
|
|
5
|
+
Tag a Trello card **agent-ready**, and Claude reads the task, edits the code, opens a PR, and pings you on Telegram — twice a day, automatically.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx agent-ready init
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The wizard will:
|
|
14
|
+
1. Connect your Trello board
|
|
15
|
+
2. Add your Anthropic API key
|
|
16
|
+
3. Give you a Telegram chat ID via @agent_ready_bot
|
|
17
|
+
4. Set all GitHub secrets automatically
|
|
18
|
+
5. Write the GitHub Actions workflows into your repo
|
|
19
|
+
|
|
20
|
+
Then commit and push `.github/` and you're live.
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
|
|
24
|
+
1. Add the `agent-ready` label to a Trello card in your **To Do** list
|
|
25
|
+
2. The agent runs at **9am and 5pm** (or trigger manually in GitHub Actions)
|
|
26
|
+
3. Claude reads the card title + description, identifies relevant files, writes the code
|
|
27
|
+
4. A PR is opened — nothing touches `main` without your review
|
|
28
|
+
5. You get a Telegram message with the PR link
|
|
29
|
+
6. Merge the PR → card moves to **Done** automatically
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- Node.js 18+
|
|
34
|
+
- A GitHub repository
|
|
35
|
+
- GitHub CLI (`gh`) for automatic secret setup (optional — can set manually)
|
|
36
|
+
- A Trello account
|
|
37
|
+
- An Anthropic API key
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
MIT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { init } from '../src/init.js'
|
|
4
|
+
|
|
5
|
+
const [, , command] = process.argv
|
|
6
|
+
|
|
7
|
+
switch (command) {
|
|
8
|
+
case 'init':
|
|
9
|
+
await init()
|
|
10
|
+
break
|
|
11
|
+
default:
|
|
12
|
+
console.log(`
|
|
13
|
+
agent-ready — wire an AI coding agent into any repo in 5 minutes
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
npx agent-ready init Set up the agent in your current repo
|
|
17
|
+
`)
|
|
18
|
+
process.exit(0)
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elmiristic/agent-ready",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Wire a Trello + Claude AI + GitHub Actions coding agent into any repo in 5 minutes.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-ready": "./bin/agent-ready.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "node bin/agent-ready.js init"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai",
|
|
14
|
+
"agent",
|
|
15
|
+
"trello",
|
|
16
|
+
"github-actions",
|
|
17
|
+
"claude",
|
|
18
|
+
"anthropic",
|
|
19
|
+
"automation",
|
|
20
|
+
"cli",
|
|
21
|
+
"telegram",
|
|
22
|
+
"coding-agent"
|
|
23
|
+
],
|
|
24
|
+
"author": "Amazesofts",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@inquirer/prompts": "^7.5.0",
|
|
28
|
+
"chalk": "^5.4.1",
|
|
29
|
+
"execa": "^9.5.2",
|
|
30
|
+
"fs-extra": "^11.3.0",
|
|
31
|
+
"ora": "^8.1.1"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/init.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { welcome } from './steps/welcome.js'
|
|
3
|
+
import { collectTrello } from './steps/collectTrello.js'
|
|
4
|
+
import { collectAnthropic } from './steps/collectAnthropic.js'
|
|
5
|
+
import { collectTelegram } from './steps/collectTelegram.js'
|
|
6
|
+
import { setGithubSecrets } from './steps/setGithubSecrets.js'
|
|
7
|
+
import { scaffold } from './steps/scaffold.js'
|
|
8
|
+
import { done } from './steps/done.js'
|
|
9
|
+
|
|
10
|
+
export async function init() {
|
|
11
|
+
try {
|
|
12
|
+
await welcome()
|
|
13
|
+
const trello = await collectTrello()
|
|
14
|
+
const anthropic = await collectAnthropic()
|
|
15
|
+
const telegram = await collectTelegram()
|
|
16
|
+
const secrets = { ...trello, ...anthropic, ...telegram }
|
|
17
|
+
await setGithubSecrets(secrets)
|
|
18
|
+
await scaffold()
|
|
19
|
+
await done()
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err.name === 'ExitPromptError') {
|
|
22
|
+
console.log(chalk.dim('\n Setup cancelled.'))
|
|
23
|
+
process.exit(0)
|
|
24
|
+
}
|
|
25
|
+
console.error(chalk.red('\n Error: ' + err.message))
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { password } from '@inquirer/prompts'
|
|
3
|
+
|
|
4
|
+
export async function collectAnthropic() {
|
|
5
|
+
console.log(chalk.bold(' Step 2 of 3 — Anthropic'))
|
|
6
|
+
console.log()
|
|
7
|
+
console.log(
|
|
8
|
+
chalk.dim(' 1. Go to: ') + chalk.cyan('https://console.anthropic.com/settings/keys'),
|
|
9
|
+
)
|
|
10
|
+
console.log(chalk.dim(' 2. Create a new API key and paste it below'))
|
|
11
|
+
console.log()
|
|
12
|
+
|
|
13
|
+
const apiKey = await password({
|
|
14
|
+
message: ' Anthropic API Key',
|
|
15
|
+
mask: '●',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
console.log()
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
ANTHROPIC_API_KEY: apiKey,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { input } from '@inquirer/prompts'
|
|
3
|
+
|
|
4
|
+
export async function collectTelegram() {
|
|
5
|
+
console.log(chalk.bold(' Step 3 of 3 — Telegram'))
|
|
6
|
+
console.log()
|
|
7
|
+
console.log(chalk.dim(' Open Telegram and message: ') + chalk.cyan('@agentreadybot'))
|
|
8
|
+
console.log()
|
|
9
|
+
console.log(chalk.dim(' Send: ') + chalk.white('/start'))
|
|
10
|
+
console.log()
|
|
11
|
+
console.log(chalk.dim(' The bot will reply with your Chat ID. Paste it below.'))
|
|
12
|
+
console.log()
|
|
13
|
+
|
|
14
|
+
const chatId = await input({
|
|
15
|
+
message: ' Your Telegram Chat ID',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
console.log()
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
TELEGRAM_BOT_TOKEN: 'managed',
|
|
22
|
+
TELEGRAM_CHAT_ID: chatId,
|
|
23
|
+
AGENT_READY_SECRET: 'ar_shared_2026',
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { input, password, confirm } from '@inquirer/prompts'
|
|
3
|
+
|
|
4
|
+
export async function collectTrello() {
|
|
5
|
+
console.log(chalk.bold(' Step 1 of 3 — Trello'))
|
|
6
|
+
console.log()
|
|
7
|
+
console.log(
|
|
8
|
+
chalk.dim(' 1. Go to: ') +
|
|
9
|
+
chalk.cyan('https://trello.com/power-ups/admin'),
|
|
10
|
+
)
|
|
11
|
+
console.log(chalk.dim(' 2. Create a Power-Up → go to API Key tab'))
|
|
12
|
+
console.log(chalk.dim(' 3. Copy your Key, then click "Token" to generate your Token'))
|
|
13
|
+
console.log()
|
|
14
|
+
|
|
15
|
+
const apiKey = await password({
|
|
16
|
+
message: ' Trello API Key',
|
|
17
|
+
mask: '●',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const token = await password({
|
|
21
|
+
message: ' Trello Token',
|
|
22
|
+
mask: '●',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
console.log()
|
|
26
|
+
console.log(chalk.dim(' To find your List IDs, open this URL in your browser:'))
|
|
27
|
+
console.log(
|
|
28
|
+
chalk.cyan(
|
|
29
|
+
` https://api.trello.com/1/members/me/boards?key=${apiKey}&token=${token}`,
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
console.log(chalk.dim(' Copy your board ID, then open:'))
|
|
33
|
+
console.log(
|
|
34
|
+
chalk.cyan(
|
|
35
|
+
` https://api.trello.com/1/boards/BOARD_ID/lists?key=${apiKey}&token=${token}`,
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
console.log()
|
|
39
|
+
|
|
40
|
+
const todoListId = await input({
|
|
41
|
+
message: ' To Do list ID',
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const doingListId = await input({
|
|
45
|
+
message: ' Doing list ID',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const doneListId = await input({
|
|
49
|
+
message: ' Done list ID',
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
console.log()
|
|
53
|
+
console.log(chalk.dim(' Now create a label called "agent-ready" on your board.'))
|
|
54
|
+
console.log(chalk.dim(' Then open this URL to find its ID:'))
|
|
55
|
+
console.log(
|
|
56
|
+
chalk.cyan(
|
|
57
|
+
` https://api.trello.com/1/boards/BOARD_ID/labels?key=${apiKey}&token=${token}`,
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
console.log()
|
|
61
|
+
|
|
62
|
+
const labelId = await input({
|
|
63
|
+
message: ' agent-ready label ID',
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
console.log()
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
TRELLO_API_KEY: apiKey,
|
|
70
|
+
TRELLO_TOKEN: token,
|
|
71
|
+
TRELLO_LIST_ID: todoListId,
|
|
72
|
+
TRELLO_DOING_LIST_ID: doingListId,
|
|
73
|
+
TRELLO_DONE_LIST_ID: doneListId,
|
|
74
|
+
TRELLO_AGENT_LABEL_ID: labelId,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
export async function done() {
|
|
4
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────────'))
|
|
5
|
+
console.log()
|
|
6
|
+
console.log(' ' + chalk.green('✓') + chalk.bold(' agent-ready is set up.'))
|
|
7
|
+
console.log()
|
|
8
|
+
console.log(chalk.dim(' How it works:'))
|
|
9
|
+
console.log(
|
|
10
|
+
chalk.dim(' 1. Add the ') +
|
|
11
|
+
chalk.yellow('agent-ready') +
|
|
12
|
+
chalk.dim(' label to a Trello card in your To Do list'),
|
|
13
|
+
)
|
|
14
|
+
console.log(
|
|
15
|
+
chalk.dim(' 2. The agent runs at 9am and 5pm (or trigger manually in GitHub Actions)'),
|
|
16
|
+
)
|
|
17
|
+
console.log(chalk.dim(' 3. Claude reads the task, edits the code, opens a PR'))
|
|
18
|
+
console.log(chalk.dim(' 4. You get a Telegram message with the PR link'))
|
|
19
|
+
console.log(chalk.dim(' 5. Review and merge — nothing touches main without you'))
|
|
20
|
+
console.log()
|
|
21
|
+
console.log(chalk.dim(' Commit the generated files and push to get started:'))
|
|
22
|
+
console.log()
|
|
23
|
+
console.log(' ' + chalk.cyan('git add .github && git commit -m "feat: add agent-ready" && git push'))
|
|
24
|
+
console.log()
|
|
25
|
+
console.log(
|
|
26
|
+
chalk.dim(' Docs: ') + chalk.cyan('https://github.com/elmir-mammadli/agent-ready'),
|
|
27
|
+
)
|
|
28
|
+
console.log()
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import fs from 'fs-extra'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import ora from 'ora'
|
|
5
|
+
import { workflowTemplate } from '../templates/workflow.js'
|
|
6
|
+
import { agentScriptTemplate } from '../templates/agentScript.js'
|
|
7
|
+
import { doneWorkflowTemplate } from '../templates/doneWorkflow.js'
|
|
8
|
+
|
|
9
|
+
export async function scaffold() {
|
|
10
|
+
const spinner = ora({ text: ' Generating workflow files...', indent: 2 }).start()
|
|
11
|
+
|
|
12
|
+
const cwd = process.cwd()
|
|
13
|
+
|
|
14
|
+
const workflowsDir = path.join(cwd, '.github', 'workflows')
|
|
15
|
+
const scriptsDir = path.join(cwd, '.github', 'scripts')
|
|
16
|
+
|
|
17
|
+
await fs.ensureDir(workflowsDir)
|
|
18
|
+
await fs.ensureDir(scriptsDir)
|
|
19
|
+
|
|
20
|
+
await fs.writeFile(
|
|
21
|
+
path.join(workflowsDir, 'agent-ready.yml'),
|
|
22
|
+
workflowTemplate(),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
await fs.writeFile(
|
|
26
|
+
path.join(workflowsDir, 'agent-ready-done.yml'),
|
|
27
|
+
doneWorkflowTemplate(),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
await fs.writeFile(
|
|
31
|
+
path.join(scriptsDir, 'agent-ready.mjs'),
|
|
32
|
+
agentScriptTemplate(),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
spinner.succeed(' Workflow files generated.')
|
|
36
|
+
console.log()
|
|
37
|
+
console.log(
|
|
38
|
+
chalk.dim(' Created: ') + chalk.white('.github/workflows/agent-ready.yml'),
|
|
39
|
+
)
|
|
40
|
+
console.log(
|
|
41
|
+
chalk.dim(' Created: ') +
|
|
42
|
+
chalk.white('.github/workflows/agent-ready-done.yml'),
|
|
43
|
+
)
|
|
44
|
+
console.log(
|
|
45
|
+
chalk.dim(' Created: ') + chalk.white('.github/scripts/agent-ready.mjs'),
|
|
46
|
+
)
|
|
47
|
+
console.log()
|
|
48
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { confirm } from '@inquirer/prompts'
|
|
3
|
+
import { execa } from 'execa'
|
|
4
|
+
import ora from 'ora'
|
|
5
|
+
|
|
6
|
+
export async function setGithubSecrets(secrets) {
|
|
7
|
+
console.log(chalk.bold(' GitHub Secrets'))
|
|
8
|
+
console.log()
|
|
9
|
+
|
|
10
|
+
const useGhCli = await confirm({
|
|
11
|
+
message:
|
|
12
|
+
' Set GitHub secrets automatically? (requires GitHub CLI — gh.io/cli)',
|
|
13
|
+
default: true,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
console.log()
|
|
17
|
+
|
|
18
|
+
const SKIP_DISPLAY = new Set(['TELEGRAM_BOT_TOKEN', 'AGENT_READY_SECRET'])
|
|
19
|
+
|
|
20
|
+
if (!useGhCli) {
|
|
21
|
+
console.log(chalk.dim(' Add these secrets manually in:'))
|
|
22
|
+
console.log(
|
|
23
|
+
chalk.dim(' GitHub repo → Settings → Secrets → Actions → New secret'),
|
|
24
|
+
)
|
|
25
|
+
console.log()
|
|
26
|
+
for (const [key] of Object.entries(secrets)) {
|
|
27
|
+
if (SKIP_DISPLAY.has(key)) continue
|
|
28
|
+
console.log(` ${chalk.yellow(key)}`)
|
|
29
|
+
}
|
|
30
|
+
console.log()
|
|
31
|
+
console.log(
|
|
32
|
+
chalk.dim(' Note: TELEGRAM_BOT_TOKEN and AGENT_READY_SECRET are managed by agent-ready.'),
|
|
33
|
+
)
|
|
34
|
+
console.log()
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const spinner = ora({ text: ' Setting GitHub secrets...', indent: 2 }).start()
|
|
39
|
+
|
|
40
|
+
let failed = []
|
|
41
|
+
|
|
42
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
43
|
+
if (key === 'TELEGRAM_BOT_TOKEN') continue
|
|
44
|
+
try {
|
|
45
|
+
await execa('gh', ['secret', 'set', key, '--body', value])
|
|
46
|
+
} catch {
|
|
47
|
+
failed.push(key)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (failed.length > 0) {
|
|
52
|
+
spinner.warn(
|
|
53
|
+
` Some secrets failed to set: ${failed.join(', ')}\n Set them manually in GitHub repo → Settings → Secrets → Actions`,
|
|
54
|
+
)
|
|
55
|
+
} else {
|
|
56
|
+
spinner.succeed(' All secrets set.')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log()
|
|
60
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
export async function welcome() {
|
|
4
|
+
console.log()
|
|
5
|
+
console.log(
|
|
6
|
+
chalk.bold(' agent-ready') + chalk.dim(' — AI coding agent for your repo'),
|
|
7
|
+
)
|
|
8
|
+
console.log()
|
|
9
|
+
console.log(
|
|
10
|
+
chalk.dim(' Tag a Trello card ') +
|
|
11
|
+
chalk.yellow('agent-ready') +
|
|
12
|
+
chalk.dim(', Claude reads the task,'),
|
|
13
|
+
)
|
|
14
|
+
console.log(
|
|
15
|
+
chalk.dim(' writes the code, opens a PR, and pings you on Telegram.'),
|
|
16
|
+
)
|
|
17
|
+
console.log()
|
|
18
|
+
console.log(chalk.dim(' This wizard sets everything up in about 5 minutes.'))
|
|
19
|
+
console.log()
|
|
20
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────────'))
|
|
21
|
+
console.log()
|
|
22
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
export function agentScriptTemplate() {
|
|
2
|
+
return `import { execSync } from 'child_process'
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
4
|
+
import { dirname } from 'path'
|
|
5
|
+
import Anthropic from '@anthropic-ai/sdk'
|
|
6
|
+
|
|
7
|
+
const AGENT_READY_API = 'https://telegram-bot-ruby-tau.vercel.app'
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
TRELLO_API_KEY,
|
|
11
|
+
TRELLO_TOKEN,
|
|
12
|
+
TRELLO_LIST_ID,
|
|
13
|
+
TRELLO_DOING_LIST_ID,
|
|
14
|
+
TRELLO_AGENT_LABEL_ID,
|
|
15
|
+
ANTHROPIC_API_KEY,
|
|
16
|
+
TELEGRAM_CHAT_ID,
|
|
17
|
+
AGENT_READY_SECRET,
|
|
18
|
+
GITHUB_TOKEN,
|
|
19
|
+
GITHUB_REPOSITORY,
|
|
20
|
+
} = process.env
|
|
21
|
+
|
|
22
|
+
const required = {
|
|
23
|
+
TRELLO_API_KEY, TRELLO_TOKEN, TRELLO_LIST_ID,
|
|
24
|
+
TRELLO_DOING_LIST_ID, TRELLO_AGENT_LABEL_ID,
|
|
25
|
+
ANTHROPIC_API_KEY, TELEGRAM_CHAT_ID,
|
|
26
|
+
GITHUB_TOKEN, GITHUB_REPOSITORY,
|
|
27
|
+
}
|
|
28
|
+
const missing = Object.entries(required).filter(([, v]) => !v).map(([k]) => k)
|
|
29
|
+
if (missing.length > 0) {
|
|
30
|
+
console.error('Missing secrets: ' + missing.join(', '))
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const anthropic = new Anthropic({ apiKey: ANTHROPIC_API_KEY })
|
|
35
|
+
|
|
36
|
+
async function trello(path, method = 'GET', body = null) {
|
|
37
|
+
const sep = path.includes('?') ? '&' : '?'
|
|
38
|
+
const url = \`https://api.trello.com/1\${path}\${sep}key=\${TRELLO_API_KEY}&token=\${TRELLO_TOKEN}\`
|
|
39
|
+
const opts = { method, headers: { 'Content-Type': 'application/json' } }
|
|
40
|
+
if (body) opts.body = JSON.stringify(body)
|
|
41
|
+
const res = await fetch(url, opts)
|
|
42
|
+
if (!res.ok) throw new Error(\`Trello \${method} \${path}: \${res.status} \${await res.text()}\`)
|
|
43
|
+
return res.json()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function sendTelegram(text) {
|
|
47
|
+
const headers = { 'Content-Type': 'application/json' }
|
|
48
|
+
if (AGENT_READY_SECRET) headers['x-internal-secret'] = AGENT_READY_SECRET
|
|
49
|
+
await fetch(\`\${AGENT_READY_API}/api/send\`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers,
|
|
52
|
+
body: JSON.stringify({ chat_id: TELEGRAM_CHAT_ID, text }),
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseClaudeJson(msg) {
|
|
57
|
+
if (msg.stop_reason === 'max_tokens') {
|
|
58
|
+
throw new Error('Claude response was cut off. Narrow the task scope in the card description.')
|
|
59
|
+
}
|
|
60
|
+
const raw = msg.content[0].text.trim()
|
|
61
|
+
const json = raw.replace(/^\`\`\`(?:json)?\\s*/i, '').replace(/\\s*\`\`\`$/, '')
|
|
62
|
+
return JSON.parse(json)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getFileTree() {
|
|
66
|
+
return execSync(
|
|
67
|
+
'find src -type f \\\\( -name "*.tsx" -o -name "*.ts" -o -name "*.css" -o -name "*.js" \\\\) | grep -v ".next" | grep -v "node_modules" | sort',
|
|
68
|
+
{ encoding: 'utf8' },
|
|
69
|
+
).trim()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function getRelevantFiles(card, fileTree) {
|
|
73
|
+
const msg = await anthropic.messages.create({
|
|
74
|
+
model: 'claude-sonnet-4-6',
|
|
75
|
+
max_tokens: 512,
|
|
76
|
+
messages: [{
|
|
77
|
+
role: 'user',
|
|
78
|
+
content: \`You are a coding agent. Given a task and file tree, identify which files to read.
|
|
79
|
+
|
|
80
|
+
Task: \${card.name}
|
|
81
|
+
Description: \${card.desc || 'No description.'}
|
|
82
|
+
|
|
83
|
+
File tree:
|
|
84
|
+
\${fileTree}
|
|
85
|
+
|
|
86
|
+
Return ONLY a JSON array of file paths, max 6 files.
|
|
87
|
+
Example: ["src/app/page.tsx", "src/components/Hero.tsx"]\`,
|
|
88
|
+
}],
|
|
89
|
+
})
|
|
90
|
+
return parseClaudeJson(msg)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function makeChanges(card, fileContents) {
|
|
94
|
+
const filesText = Object.entries(fileContents)
|
|
95
|
+
.map(([path, content]) => \`### \${path}\\n\\\`\\\`\\\`\\n\${content}\\n\\\`\\\`\\\`\`)
|
|
96
|
+
.join('\\n\\n')
|
|
97
|
+
|
|
98
|
+
const msg = await anthropic.messages.create({
|
|
99
|
+
model: 'claude-sonnet-4-6',
|
|
100
|
+
max_tokens: 16000,
|
|
101
|
+
messages: [{
|
|
102
|
+
role: 'user',
|
|
103
|
+
content: \`You are a coding agent.
|
|
104
|
+
|
|
105
|
+
Task: \${card.name}
|
|
106
|
+
Description: \${card.desc || 'No description.'}
|
|
107
|
+
|
|
108
|
+
File contents:
|
|
109
|
+
\${filesText}
|
|
110
|
+
|
|
111
|
+
Return ONLY valid JSON — no markdown, no explanation:
|
|
112
|
+
{
|
|
113
|
+
"changes": [{"path": "src/...", "content": "...complete file content..."}],
|
|
114
|
+
"summary": "One sentence describing the change",
|
|
115
|
+
"branchName": "lowercase-hyphen-slug-max-40-chars"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
Rules:
|
|
119
|
+
- Only include files you actually modified
|
|
120
|
+
- Return complete file content, not a diff
|
|
121
|
+
- branchName: lowercase, hyphens only, max 40 chars\`,
|
|
122
|
+
}],
|
|
123
|
+
})
|
|
124
|
+
return parseClaudeJson(msg)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function createPR(branch, card, summary) {
|
|
128
|
+
const [owner, repo] = GITHUB_REPOSITORY.split('/')
|
|
129
|
+
const res = await fetch(\`https://api.github.com/repos/\${owner}/\${repo}/pulls\`, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: {
|
|
132
|
+
Authorization: \`Bearer \${GITHUB_TOKEN}\`,
|
|
133
|
+
'Content-Type': 'application/json',
|
|
134
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
title: \`agent: \${card.name}\`,
|
|
138
|
+
body: \`## Summary\\n\${summary}\\n\\n## Trello Card\\n\${card.shortUrl}\\n\\n<!-- trello-card-id: \${card.id} -->\`,
|
|
139
|
+
head: branch,
|
|
140
|
+
base: 'main',
|
|
141
|
+
}),
|
|
142
|
+
})
|
|
143
|
+
if (!res.ok) throw new Error(\`GitHub PR: \${res.status} \${await res.text()}\`)
|
|
144
|
+
return (await res.json()).html_url
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function main() {
|
|
148
|
+
console.log('Fetching Trello cards...')
|
|
149
|
+
const allCards = await trello(\`/lists/\${TRELLO_LIST_ID}/cards\`)
|
|
150
|
+
const cards = allCards.filter(c => c.idLabels.includes(TRELLO_AGENT_LABEL_ID))
|
|
151
|
+
|
|
152
|
+
console.log(\`\${allCards.length} total, \${cards.length} agent-ready.\`)
|
|
153
|
+
|
|
154
|
+
if (cards.length === 0) {
|
|
155
|
+
await sendTelegram('agent-ready: No agent-ready tasks found. All clear.')
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const card = cards[0]
|
|
160
|
+
console.log(\`Processing: "\${card.name}"\`)
|
|
161
|
+
|
|
162
|
+
await trello(\`/cards/\${card.id}?idList=\${TRELLO_DOING_LIST_ID}\`, 'PUT')
|
|
163
|
+
await sendTelegram(\`agent-ready: Starting task\\n\${card.name}\\n\\nWorking on it...\`)
|
|
164
|
+
|
|
165
|
+
execSync('git config user.name "agent-ready"')
|
|
166
|
+
execSync('git config user.email "agent@agent-ready.dev"')
|
|
167
|
+
execSync(\`git remote set-url origin https://x-access-token:\${GITHUB_TOKEN}@github.com/\${GITHUB_REPOSITORY}.git\`)
|
|
168
|
+
|
|
169
|
+
const fileTree = getFileTree()
|
|
170
|
+
|
|
171
|
+
console.log('Identifying relevant files...')
|
|
172
|
+
const filePaths = await getRelevantFiles(card, fileTree)
|
|
173
|
+
console.log('Files:', filePaths)
|
|
174
|
+
|
|
175
|
+
const fileContents = {}
|
|
176
|
+
for (const filePath of filePaths) {
|
|
177
|
+
try {
|
|
178
|
+
const content = readFileSync(filePath, 'utf8')
|
|
179
|
+
const lines = content.split('\\n')
|
|
180
|
+
fileContents[filePath] = lines.length > 300
|
|
181
|
+
? lines.slice(0, 300).join('\\n') + '\\n// ... truncated ...'
|
|
182
|
+
: content
|
|
183
|
+
} catch {
|
|
184
|
+
console.warn(\`Could not read \${filePath}\`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log('Generating changes...')
|
|
189
|
+
const { changes, summary, branchName } = await makeChanges(card, fileContents)
|
|
190
|
+
console.log('Summary:', summary)
|
|
191
|
+
|
|
192
|
+
const branch = \`agent/\${card.id}_\${branchName}\`
|
|
193
|
+
execSync(\`git checkout -b \${branch}\`)
|
|
194
|
+
|
|
195
|
+
for (const { path, content } of changes) {
|
|
196
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
197
|
+
writeFileSync(path, content, 'utf8')
|
|
198
|
+
console.log('Written:', path)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
execSync('git add -A')
|
|
202
|
+
execSync(\`git commit -m "agent: \${summary}"\`)
|
|
203
|
+
execSync(\`git push origin \${branch}\`)
|
|
204
|
+
|
|
205
|
+
const prUrl = await createPR(branch, card, summary)
|
|
206
|
+
console.log('PR:', prUrl)
|
|
207
|
+
|
|
208
|
+
await sendTelegram(
|
|
209
|
+
\`agent-ready: Task complete\\n\\n\` +
|
|
210
|
+
\`Task: \${card.name}\\n\` +
|
|
211
|
+
\`Done: \${summary}\\n\` +
|
|
212
|
+
\`PR: \${prUrl}\\n\\n\` +
|
|
213
|
+
\`Review and merge when ready.\`
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
console.log('Done.')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
main().catch(async err => {
|
|
220
|
+
console.error('Fatal:', err.message)
|
|
221
|
+
await sendTelegram(\`agent-ready: Error\\n\${err.message}\`).catch(() => {})
|
|
222
|
+
process.exit(1)
|
|
223
|
+
})
|
|
224
|
+
`
|
|
225
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function doneWorkflowTemplate() {
|
|
2
|
+
return `name: agent-ready — mark done
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
pull_request:
|
|
6
|
+
types: [closed]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
mark-done:
|
|
10
|
+
if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'agent/')
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Move Trello card to Done and notify Telegram
|
|
15
|
+
run: |
|
|
16
|
+
CARD_ID=$(echo "$PR_BODY" | grep -o 'trello-card-id: [a-f0-9]*' | awk '{print $2}')
|
|
17
|
+
|
|
18
|
+
if [ -z "$CARD_ID" ]; then
|
|
19
|
+
echo "No card ID found. Skipping."
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
curl -s -X PUT \\
|
|
24
|
+
"https://api.trello.com/1/cards/\${CARD_ID}?idList=\${TRELLO_DONE_LIST_ID}&key=\${TRELLO_API_KEY}&token=\${TRELLO_TOKEN}"
|
|
25
|
+
|
|
26
|
+
MESSAGE="Merged: \${PR_TITLE}%0ACard moved to Done."
|
|
27
|
+
curl -s -X POST "https://telegram-bot-ruby-tau.vercel.app/api/send" \\
|
|
28
|
+
-H "Content-Type: application/json" \\
|
|
29
|
+
-d "{\\"chat_id\\":\\"\${TELEGRAM_CHAT_ID}\\",\\"text\\":\\"\${MESSAGE}\\"}"
|
|
30
|
+
env:
|
|
31
|
+
PR_BODY: \${{ github.event.pull_request.body }}
|
|
32
|
+
PR_TITLE: \${{ github.event.pull_request.title }}
|
|
33
|
+
TRELLO_API_KEY: \${{ secrets.TRELLO_API_KEY }}
|
|
34
|
+
TRELLO_TOKEN: \${{ secrets.TRELLO_TOKEN }}
|
|
35
|
+
TRELLO_DONE_LIST_ID: \${{ secrets.TRELLO_DONE_LIST_ID }}
|
|
36
|
+
TELEGRAM_CHAT_ID: \${{ secrets.TELEGRAM_CHAT_ID }}
|
|
37
|
+
`
|
|
38
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function workflowTemplate() {
|
|
2
|
+
return `name: agent-ready
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
schedule:
|
|
6
|
+
- cron: '0 9 * * *'
|
|
7
|
+
- cron: '0 17 * * *'
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: write
|
|
12
|
+
pull-requests: write
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
run-agent:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
timeout-minutes: 15
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: '20'
|
|
25
|
+
|
|
26
|
+
- name: Install agent dependencies
|
|
27
|
+
run: npm install --prefix .github/scripts @anthropic-ai/sdk
|
|
28
|
+
|
|
29
|
+
- name: Run agent
|
|
30
|
+
run: node .github/scripts/agent-ready.mjs
|
|
31
|
+
env:
|
|
32
|
+
TRELLO_API_KEY: \${{ secrets.TRELLO_API_KEY }}
|
|
33
|
+
TRELLO_TOKEN: \${{ secrets.TRELLO_TOKEN }}
|
|
34
|
+
TRELLO_LIST_ID: \${{ secrets.TRELLO_LIST_ID }}
|
|
35
|
+
TRELLO_DOING_LIST_ID: \${{ secrets.TRELLO_DOING_LIST_ID }}
|
|
36
|
+
TRELLO_AGENT_LABEL_ID: \${{ secrets.TRELLO_AGENT_LABEL_ID }}
|
|
37
|
+
ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
|
|
38
|
+
TELEGRAM_CHAT_ID: \${{ secrets.TELEGRAM_CHAT_ID }}
|
|
39
|
+
AGENT_READY_SECRET: \${{ secrets.AGENT_READY_SECRET }}
|
|
40
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
41
|
+
`
|
|
42
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN
|
|
2
|
+
const INTERNAL_SECRET = process.env.INTERNAL_SECRET
|
|
3
|
+
|
|
4
|
+
export default async function handler(req, res) {
|
|
5
|
+
if (req.method !== 'POST') {
|
|
6
|
+
return res.status(405).json({ error: 'Method not allowed' })
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const authHeader = req.headers['x-internal-secret']
|
|
10
|
+
if (INTERNAL_SECRET && authHeader !== INTERNAL_SECRET) {
|
|
11
|
+
return res.status(401).json({ error: 'Unauthorized' })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let body
|
|
15
|
+
try {
|
|
16
|
+
body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
|
17
|
+
} catch {
|
|
18
|
+
return res.status(400).json({ error: 'Invalid JSON' })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { chat_id, text } = body
|
|
22
|
+
if (!chat_id || !text) {
|
|
23
|
+
return res.status(400).json({ error: 'chat_id and text required' })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const tgRes = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({ chat_id, text, parse_mode: 'HTML' }),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const tgData = await tgRes.json()
|
|
33
|
+
if (!tgRes.ok) {
|
|
34
|
+
return res.status(502).json({ error: 'Telegram error', detail: tgData })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return res.status(200).json({ ok: true })
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN
|
|
2
|
+
|
|
3
|
+
function tg(method, body) {
|
|
4
|
+
return fetch(`https://api.telegram.org/bot${BOT_TOKEN}/${method}`, {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
headers: { 'Content-Type': 'application/json' },
|
|
7
|
+
body: JSON.stringify(body),
|
|
8
|
+
})
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default async function handler(req, res) {
|
|
12
|
+
if (req.method !== 'POST') {
|
|
13
|
+
return res.status(405).json({ error: 'Method not allowed' })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let body
|
|
17
|
+
try {
|
|
18
|
+
body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
|
19
|
+
} catch {
|
|
20
|
+
return res.status(400).json({ error: 'Invalid JSON' })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const message = body?.message
|
|
24
|
+
if (!message) return res.status(200).json({ ok: true })
|
|
25
|
+
|
|
26
|
+
const chatId = message.chat.id
|
|
27
|
+
const text = message.text?.trim()
|
|
28
|
+
|
|
29
|
+
if (text === '/start') {
|
|
30
|
+
await tg('sendMessage', {
|
|
31
|
+
chat_id: chatId,
|
|
32
|
+
parse_mode: 'HTML',
|
|
33
|
+
text:
|
|
34
|
+
`👋 <b>Welcome to agent-ready!</b>\n\n` +
|
|
35
|
+
`Your Telegram Chat ID is:\n\n` +
|
|
36
|
+
`<code>${chatId}</code>\n\n` +
|
|
37
|
+
`Copy this and paste it when running <code>npx agent-ready init</code>.\n\n` +
|
|
38
|
+
`You'll receive notifications here whenever your coding agent finishes a task or opens a PR.`,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return res.status(200).json({ ok: true })
|
|
43
|
+
}
|