Compare commits
5 Commits
ff73115efc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 84c364693c | |||
| 078b3b8174 | |||
| 30c4a9a088 | |||
| 229312323d | |||
| 862c600152 |
63
README.md
63
README.md
@@ -2,18 +2,75 @@
|
||||
<img src="assets/DevClaw.png" width="300" alt="DevClaw Logo">
|
||||
</p>
|
||||
|
||||
# DevClaw — Development Plugin for OpenClaw
|
||||
# DevClaw — Development Plugin for OpenClaw (Gitea Fork)
|
||||
|
||||
**Fork with Gitea support for OpenClaw development orchestration.**
|
||||
|
||||
> **Note:** This is a fork of the original [DevClaw](https://github.com/laurentenhoor/devclaw) with added **Gitea provider support**. Use this version if you use Gitea instead of GitHub/GitLab.
|
||||
|
||||
**Turn any group chat into a dev team that ships.**
|
||||
|
||||
DevClaw is a plugin for [OpenClaw](https://openclaw.ai) that turns your orchestrator agent into a development manager. It hires developers, assigns tasks, reviews code, and keeps the pipeline moving — across as many projects as you have group chats.
|
||||
|
||||
**Prerequisites:** [OpenClaw](https://openclaw.ai) must be installed and running.
|
||||
## 🆕 Gitea Support
|
||||
|
||||
This fork adds support for **Gitea** issue tracking alongside GitHub and GitLab. The Gitea provider uses the Gitea REST API with token authentication.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
openclaw plugins install @laurentenhoor/devclaw
|
||||
# Install from our Gitea fork
|
||||
openclaw plugins install https://pfoster.dynu.net/peter/devclaw-gitea.git
|
||||
|
||||
# Or clone and install locally
|
||||
git clone https://pfoster.dynu.net/peter/devclaw-gitea.git
|
||||
openclaw plugins install -l ./devclaw-gitea
|
||||
```
|
||||
|
||||
### Configuration for Gitea
|
||||
|
||||
1. **Set Gitea Token:**
|
||||
```bash
|
||||
export GITEA_TOKEN=your_gitea_personal_access_token
|
||||
```
|
||||
|
||||
Or add to systemd override:
|
||||
```bash
|
||||
mkdir -p ~/.config/systemd/user/openclaw-gateway.service.d
|
||||
echo -e '[Service]\nEnvironment="GITEA_TOKEN=your_token"' > ~/.config/systemd/user/openclaw-gateway.service.d/override.conf
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart openclaw-gateway
|
||||
```
|
||||
|
||||
2. **Automatic Detection:**
|
||||
- The Gitea provider automatically detects your Gitea instance from git remote URL
|
||||
- Parse from `.git/config`: `https://your-gitea-domain.com/owner/repo.git`
|
||||
- Fallback to default if parsing fails
|
||||
|
||||
3. **Project Registration:**
|
||||
When registering a project with DevClaw:
|
||||
```
|
||||
/dev register my-project /path/to/repo development
|
||||
```
|
||||
The provider auto-detects Gitea and configures accordingly.
|
||||
|
||||
### Gitea-Specific Features
|
||||
|
||||
- ✅ Auto-creates DevClaw workflow labels (Planning, To Do, Doing, To Test, Testing, To Improve, Refining, Done, To Design, Designing)
|
||||
- ✅ Dynamic label ID fetching with fallback
|
||||
- ✅ Issue creation, reading, and label transitions
|
||||
- ✅ Comment management
|
||||
- ✅ Retry logic with exponential backoff
|
||||
- ✅ Clear error messages for troubleshooting
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- Label transitions sometimes require manual refresh (edge case under investigation)
|
||||
- Recommend checking Gitea UI if label doesn't update immediately
|
||||
- PR/MR detection not implemented for Gitea (use issue tracking only)
|
||||
|
||||
**Prerequisites:** [OpenClaw](https://openclaw.ai) must be installed and running.
|
||||
|
||||
Then start onboarding by chatting with your agent in any channel:
|
||||
```
|
||||
"Hey, can you help me set up DevClaw?"
|
||||
|
||||
@@ -60,45 +60,108 @@ export class GiteaProvider implements IssueProvider {
|
||||
this.workflow = opts.workflow ?? DEFAULT_WORKFLOW;
|
||||
|
||||
// Extract repo info from git remote
|
||||
// For now, hardcode for our Gitea instance
|
||||
this.baseUrl = "http://192.168.1.150:3000";
|
||||
this.token = "1e61c82328feb943f0b9d466ccd2c1eceefb3ee8"; // Our Gitea token
|
||||
this.extractRepoInfoFromRemote();
|
||||
|
||||
// Get token from environment variable
|
||||
this.token = process.env.GITEA_TOKEN || "";
|
||||
|
||||
if (!this.token) {
|
||||
console.warn("GITEA_TOKEN environment variable not set. Gitea API calls may fail.");
|
||||
}
|
||||
}
|
||||
|
||||
private extractRepoInfoFromRemote(): void {
|
||||
try {
|
||||
// Try to parse .git/config file directly (synchronous)
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const gitConfigPath = path.join(this.repoPath, '.git', 'config');
|
||||
|
||||
if (fs.existsSync(gitConfigPath)) {
|
||||
const configContent = fs.readFileSync(gitConfigPath, 'utf8');
|
||||
const urlMatch = configContent.match(/url\s*=\s*(https?:\/\/[^\s]+)/);
|
||||
|
||||
if (urlMatch) {
|
||||
const remoteUrl = urlMatch[1];
|
||||
// Parse Gitea URL (e.g., https://pfoster.dynu.net/peter/clawd.git)
|
||||
// or http://192.168.1.150:3000/peter/clawd.git
|
||||
const repoMatch = remoteUrl.match(/^(https?:\/\/[^\/]+)\/([^\/]+)\/([^\/\.]+)(?:\.git)?$/);
|
||||
|
||||
if (repoMatch) {
|
||||
this.baseUrl = repoMatch[1];
|
||||
this.owner = repoMatch[2];
|
||||
this.repo = repoMatch[3];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to extract repo info from git config:", err);
|
||||
}
|
||||
|
||||
// Fallback to hardcoded values for testing
|
||||
this.baseUrl = "https://pfoster.dynu.net";
|
||||
this.owner = "peter";
|
||||
this.repo = "clawd";
|
||||
}
|
||||
|
||||
private async apiRequest(method: string, endpoint: string, data?: any): Promise<any> {
|
||||
private async apiRequest(method: string, endpoint: string, data?: any, retryCount = 0): Promise<any> {
|
||||
const maxRetries = 2;
|
||||
|
||||
// Validate we have required configuration
|
||||
if (!this.token) {
|
||||
throw new Error(`GITEA_TOKEN environment variable is not set. Required for Gitea API authentication.`);
|
||||
}
|
||||
|
||||
if (!this.baseUrl || !this.owner || !this.repo) {
|
||||
throw new Error(`Gitea repository information is incomplete. baseUrl: ${this.baseUrl}, owner: ${this.owner}, repo: ${this.repo}`);
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}/api/v1${endpoint}`;
|
||||
|
||||
const args = ["curl", "-s", "-X", method, "-H", `Authorization: token ${this.token}`];
|
||||
try {
|
||||
const args = ["curl", "-s", "-X", method, "-H", `Authorization: token ${this.token}`];
|
||||
|
||||
if (data) {
|
||||
args.push("-H", "Content-Type: application/json");
|
||||
args.push("-d", JSON.stringify(data));
|
||||
}
|
||||
|
||||
args.push(url);
|
||||
|
||||
const result = await runCommand(args, { timeoutMs: 30_000 });
|
||||
|
||||
if (result.stderr) {
|
||||
console.error(`Gitea API error for ${method} ${endpoint}:`, result.stderr);
|
||||
}
|
||||
|
||||
if (result.stdout.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
if (parsed.message && parsed.message.includes("error")) {
|
||||
throw new Error(`Gitea API error: ${parsed.message}`);
|
||||
}
|
||||
return parsed;
|
||||
} catch (err) {
|
||||
console.error(`Failed to parse JSON from Gitea API: ${result.stdout.substring(0, 200)}`);
|
||||
throw new Error(`Failed to parse JSON from Gitea API: ${(err as Error).message}`);
|
||||
if (data) {
|
||||
args.push("-H", "Content-Type: application/json");
|
||||
args.push("-d", JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
args.push(url);
|
||||
|
||||
const result = await runCommand(args, { timeoutMs: 30_000 });
|
||||
|
||||
// Check for command execution errors
|
||||
if (result.error) {
|
||||
throw new Error(`Gitea API ${method} ${endpoint} failed: ${result.error}`);
|
||||
}
|
||||
|
||||
if (result.exitCode !== undefined && result.exitCode !== 0) {
|
||||
throw new Error(`Gitea API ${method} ${endpoint} failed (exit ${result.exitCode}): ${result.stderr || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
if (result.stdout.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
if (parsed.message && parsed.message.includes("error")) {
|
||||
throw new Error(`Gitea API error: ${parsed.message}`);
|
||||
}
|
||||
return parsed;
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to parse JSON from Gitea API: ${result.stdout.substring(0, 200)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
// Retry on network errors
|
||||
if (retryCount < maxRetries && (err as Error).message.includes("failed")) {
|
||||
console.warn(`Retrying Gitea API ${method} ${endpoint} (attempt ${retryCount + 1}/${maxRetries})`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1))); // Exponential backoff
|
||||
return this.apiRequest(method, endpoint, data, retryCount + 1);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async getLabelId(labelName: string): Promise<number> {
|
||||
@@ -107,19 +170,47 @@ export class GiteaProvider implements IssueProvider {
|
||||
return this.labelCache.get(labelName)!;
|
||||
}
|
||||
|
||||
// Fetch all labels and cache them
|
||||
const labels = await this.apiRequest("GET", `/repos/${this.owner}/${this.repo}/labels`) as GiteaLabel[];
|
||||
try {
|
||||
// Fetch all labels from Gitea
|
||||
const labels = await this.apiRequest("GET", `/repos/${this.owner}/${this.repo}/labels`) as GiteaLabel[];
|
||||
|
||||
for (const label of labels) {
|
||||
this.labelCache.set(label.name, label.id);
|
||||
// Cache all labels
|
||||
for (const label of labels) {
|
||||
this.labelCache.set(label.name, label.id);
|
||||
}
|
||||
|
||||
const labelId = this.labelCache.get(labelName);
|
||||
if (!labelId) {
|
||||
throw new Error(`Label "${labelName}" not found in Gitea repo ${this.owner}/${this.repo}`);
|
||||
}
|
||||
|
||||
return labelId;
|
||||
} catch (err) {
|
||||
// Fallback to hardcoded IDs if API fails
|
||||
const errorMsg = `Failed to fetch labels from Gitea: ${(err as Error).message}`;
|
||||
|
||||
const fallbackMap: Record<string, number> = {
|
||||
"Planning": 1,
|
||||
"To Do": 2,
|
||||
"Doing": 3,
|
||||
"To Test": 4,
|
||||
"Testing": 5,
|
||||
"To Improve": 6,
|
||||
"Refining": 7,
|
||||
"Done": 8,
|
||||
"To Design": 9,
|
||||
"Designing": 10
|
||||
};
|
||||
|
||||
const labelId = fallbackMap[labelName];
|
||||
if (!labelId) {
|
||||
throw new Error(`${errorMsg}. Also, label "${labelName}" not found in fallback map.`);
|
||||
}
|
||||
|
||||
// Cache the fallback value
|
||||
this.labelCache.set(labelName, labelId);
|
||||
return labelId;
|
||||
}
|
||||
|
||||
const labelId = this.labelCache.get(labelName);
|
||||
if (!labelId) {
|
||||
throw new Error(`Label "${labelName}" not found in Gitea repo`);
|
||||
}
|
||||
|
||||
return labelId;
|
||||
}
|
||||
|
||||
async ensureLabel(name: string, color: string): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user