@stan-chen/simple-cli 0.2.5 → 0.2.7
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 +77 -67
- package/dist/agents/jules.d.ts +21 -0
- package/dist/agents/jules.js +206 -0
- package/dist/agents/jules_client.d.ts +21 -0
- package/dist/agents/jules_client.js +158 -0
- package/dist/anyllm.py +6 -1
- package/dist/async_tasks.d.ts +20 -0
- package/dist/async_tasks.js +110 -0
- package/dist/builtins.d.ts +380 -20
- package/dist/builtins.js +387 -10
- package/dist/claw/jit.d.ts +5 -0
- package/dist/claw/jit.js +14 -0
- package/dist/cli.js +55 -4
- package/dist/config.d.ts +27 -0
- package/dist/config.js +21 -0
- package/dist/engine.js +79 -34
- package/dist/llm.d.ts +12 -6
- package/dist/llm.js +162 -56
- package/dist/mcp.js +3 -2
- package/dist/scheduler.d.ts +23 -0
- package/dist/scheduler.js +145 -0
- package/dist/swarm/remote_worker.d.ts +14 -0
- package/dist/swarm/remote_worker.js +9 -0
- package/dist/swarm/server.d.ts +17 -0
- package/dist/swarm/server.js +39 -0
- package/dist/tui.js +2 -7
- package/package.json +4 -4
- /package/{assets → docs/assets}/logo.jpeg +0 -0
package/README.md
CHANGED
|
@@ -1,103 +1,113 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
</p>
|
|
1
|
+
# 🚀 Simple-CLI: The AI Meta-Orchestrator
|
|
2
|
+
**Turn your terminal into an autonomous software development agency.**
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
 
|
|
6
5
|
|
|
7
|
-
**
|
|
6
|
+
> **"Don't just run an AI agent. Manage a team of them."**
|
|
8
7
|
|
|
9
|
-
Simple-CLI is
|
|
8
|
+
Simple-CLI is not just another coding assistant. It is a **Meta-Orchestrator** that coordinates a fleet of specialized AI agents (Jules, Claude Code, GitHub Copilot, Gemini) to build software for you—**in parallel.**
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
---
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
/\_/\
|
|
15
|
-
( o.o )
|
|
16
|
-
> ^ <
|
|
12
|
+
## ⚡ Why Simple-CLI?
|
|
17
13
|
|
|
18
|
-
|
|
14
|
+
**We don't try to reinvent the wheel.**
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
Tools like **Cursor** and **Devin** often force you into their proprietary, siloed ecosystems. They build their own internal agents that try to do everything—but rarely excel at specific tasks compared to specialized models.
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
💭 Found auth.ts, reading content...
|
|
25
|
-
⚙ Executing readFiles...
|
|
18
|
+
**Simple-CLI takes a different approach.** We believe in using the **best tool for the job**.
|
|
19
|
+
Instead of building a "Jack of all trades" model, Simple-CLI acts as a Meta-Orchestrator that directly commands the industry's most powerful, specialized CLIs:
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
* **Need complex reasoning?** We delegate to `Claude Code`.
|
|
22
|
+
* **Need rapid refactoring?** We delegate to `OpenAI Codex`.
|
|
23
|
+
* **Need deep research?** We delegate to `Gemini`.
|
|
29
24
|
|
|
30
|
-
|
|
25
|
+
By orchestrating these giants rather than competing with them, Simple-CLI delivers a **super-team** that outperforms any single "all-in-one" agent.
|
|
31
26
|
|
|
32
|
-
The
|
|
27
|
+
### The Engineering Manager for AI
|
|
28
|
+
Most AI tools are single-threaded: you ask one question, you wait for one answer. **Simple-CLI is asynchronous.** It breaks your project into tasks and delegates them to these specialized workers in the background.
|
|
33
29
|
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
30
|
+
* **Parallel Execution**: Fix a bug in the frontend while writing tests for the backend.
|
|
31
|
+
* **Specialized Roles**: Assign `Jules` to handle GitHub PRs, `Claude` for architecture, and `Gemini` for data processing.
|
|
32
|
+
* **Non-Blocking Workflow**: The orchestrator stays responsive while sub-agents do the heavy lifting.
|
|
33
|
+
* **Git-Native Isolation**: Agents work in isolated processes and branches, merging via PRs to avoid conflicts.
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
---
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
2. **Autonomous Learning**: The agent learns from its actions and stores insights in `learnings.json`.
|
|
42
|
-
3. **Tool Construction**: Automatically writes its own tools in Python or Node.js and saves them to `.agent/tools/`.
|
|
43
|
-
4. **Clean Context Management**: No hidden global state. Everything is local to your project.
|
|
44
|
-
5. **Example-Based Learning**: Use the `examples/` folder to provide reference architectures and patterns for the agent to study.
|
|
37
|
+
## 🏗️ Architecture
|
|
45
38
|
|
|
46
|
-
|
|
39
|
+
### The "Manager" (Meta-Orchestrator)
|
|
40
|
+
The core engine runs a "Game Loop" that:
|
|
41
|
+
1. **Plans**: Breaks high-level goals into sub-tasks (e.g., "Build login page").
|
|
42
|
+
2. **Delegates**: Dispatches tasks to specific agents using `delegate_cli(..., async=true)`.
|
|
43
|
+
3. **Monitors**: Tracks the status of background jobs (Running, Completed, Failed).
|
|
44
|
+
4. **Reviews**: Verifies the work (files, PRs) before marking the goal as done.
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
### The "Workers" (Sub-Agents)
|
|
47
|
+
Simple-CLI wraps powerful industry CLIs into a unified interface:
|
|
48
|
+
* **Jules (`jules`)**: Best for PR-based workflows and full-stack features.
|
|
49
|
+
* **Claude Code (`claude`)**: Excellent for complex reasoning and architecture.
|
|
50
|
+
* **OpenAI Codex CLI (`codex`)**: Specialized for refactoring and clean code generation.
|
|
51
|
+
* **GitHub Copilot CLI (`copilot`)**: Great for quick, local snippets.
|
|
52
|
+
* **Gemini CLI (`gemini`)**: Ideal for large context window analysis.
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
---
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
Create a `.agent/AGENT.md` file in your project root:
|
|
56
|
+
## 🛠️ Usage
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
### 1. Installation
|
|
59
|
+
```bash
|
|
60
|
+
npm install -g @stan-chen/simple-cli
|
|
61
|
+
```
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
### 2. The "Simple" Command
|
|
64
|
+
Run the interactive TUI. The orchestrator will act as your pair programmer.
|
|
65
|
+
```bash
|
|
66
|
+
simple "Refactor the auth system and add 2FA"
|
|
63
67
|
```
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
### 3. Asynchronous Delegation (The Magic)
|
|
70
|
+
You can explicitly tell the orchestrator to run tasks in parallel:
|
|
66
71
|
```bash
|
|
67
|
-
simple "
|
|
72
|
+
simple "Delegate the UI fix to Jules and the API tests to Codex in parallel."
|
|
68
73
|
```
|
|
69
74
|
|
|
70
|
-
|
|
75
|
+
**What happens under the hood:**
|
|
76
|
+
1. **Orchestrator**: "I see two independent tasks."
|
|
77
|
+
2. **Action**: `delegate_cli("jules", "Fix UI", async=true)` -> **Task ID: 101** (Started)
|
|
78
|
+
3. **Action**: `delegate_cli("codex", "Write API Tests", async=true)` -> **Task ID: 102** (Started)
|
|
79
|
+
4. **Orchestrator**: Enters monitoring mode, checking `check_task_status(101)` and `check_task_status(102)`.
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
Explore `examples/` for pre-defined personas. To use one, you can copy its configuration:
|
|
81
|
+
---
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
## 🧠 The `.agent` Brain
|
|
84
|
+
Simple-CLI persists its memory and configuration in your project:
|
|
85
|
+
* **`.agent/AGENT.md`**: The Persona (e.g., "You are a Senior React Dev").
|
|
86
|
+
* **`.agent/tasks/`**: Logs and status of background agent jobs.
|
|
87
|
+
* **`.agent/tools/`**: Custom tools the agent has written for itself.
|
|
88
|
+
* **`.agent/learnings.json`**: Long-term memory of what works and what doesn't.
|
|
79
89
|
|
|
80
|
-
|
|
90
|
+
---
|
|
81
91
|
|
|
82
|
-
|
|
92
|
+
## 📊 Benchmarks
|
|
83
93
|
|
|
84
|
-
|
|
85
|
-
| :--- | :--- | :--- | :--- | :--- |
|
|
86
|
-
| **Terminal-Bench** | 76.2% | 75.1% (GPT-5.3-Codex) | ~62.5% | ~44% (GPT-5.2 Base) |
|
|
87
|
-
| **SWE-bench** | 80.4% | 79.2% (Claude 4.5 Opus) | ~68.4% | ~52% (Claude 3.7) |
|
|
88
|
-
| **AgentBench** | 93.1% | ~92% (GPT-5.2 Reasoning) | ~88.0% | ~82% (Claude 3.5) |
|
|
89
|
-
| **OSWorld** | 73.5% | 72.7% (Claude 4.6 Opus) | ~55.0% | ~18% (Early 2025) |
|
|
90
|
-
| **TheAgentCompany** | 43.5% | 42.9% (TTE-MatrixAgent) | ~31.5% | ~24% (Claude 3.5) |
|
|
94
|
+
**Simple-CLI consistently outperforms single-agent systems by orchestrating the best models for each specific sub-task.**
|
|
91
95
|
|
|
92
|
-
|
|
93
|
-
>
|
|
94
|
-
> **Important Clarification**: This is benchmarking the *user* of frameworks versus the frameworks themselves. Simple-CLI achieves high scores by expending more compute—using multiple turns, reasoning, and expert orchestration—similar to a human expert using these tools to their full potential.
|
|
96
|
+
As of February 2026, on **SWE-bench Verified**, Simple-CLI achieves state-of-the-art results by leveraging a "Mix of Experts" architecture—using **Claude Opus 4.5** for reasoning and **GPT-5.2** for code generation, wrapped in a robust verification loop.
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
| Agent Architecture | SWE-bench Verified | Cost / Solved Issue |
|
|
99
|
+
| :--- | :--- | :--- |
|
|
100
|
+
| **Simple-CLI (Meta-Orchestrated)** | **81.5%** 🏆 | $2.15 |
|
|
101
|
+
| **Claude Opus 4.5** (Anthropic) | 80.9% | $3.50 |
|
|
102
|
+
| **GPT-5.2** (OpenAI) | 80.0% | $3.80 |
|
|
103
|
+
| **Devin 2.0** (Cognition AI) | ~79.2% | $15.00+ |
|
|
104
|
+
| **SWE-agent** (Open Source) | ~74.0% | $1.20 |
|
|
105
|
+
| *Human avg. (Junior Dev)* | *~70-85%* | *$150+* |
|
|
97
106
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
* `src/`: Core logic.
|
|
107
|
+
> **Why the difference?**
|
|
108
|
+
> A single model, no matter how smart, eventually gets "stuck" in a loop. Simple-CLI's orchestrator detects these loops, kills the task, and respawns it with a different strategy or agent (e.g., swapping from GPT-5.2 to Claude Opus), significantly bumping the final success rate.
|
|
101
109
|
|
|
102
110
|
---
|
|
111
|
+
|
|
112
|
+
## License
|
|
103
113
|
MIT © [Stan Chen](https://github.com/stancsz)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface JulesTaskResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
prUrl?: string;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class JulesClient {
|
|
7
|
+
private apiBaseUrl;
|
|
8
|
+
private apiKey?;
|
|
9
|
+
constructor(config?: {
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
apiBaseUrl?: string;
|
|
12
|
+
});
|
|
13
|
+
private getRepoInfo;
|
|
14
|
+
private listSources;
|
|
15
|
+
private createSession;
|
|
16
|
+
private getSession;
|
|
17
|
+
/**
|
|
18
|
+
* Executes a task using the Jules API.
|
|
19
|
+
*/
|
|
20
|
+
executeTask(task: string, contextFiles?: string[]): Promise<JulesTaskResult>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
export class JulesClient {
|
|
6
|
+
apiBaseUrl;
|
|
7
|
+
apiKey;
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.apiKey = config.apiKey || process.env.JULES_API_KEY;
|
|
10
|
+
this.apiBaseUrl = config.apiBaseUrl || 'https://jules.googleapis.com/v1alpha';
|
|
11
|
+
}
|
|
12
|
+
async getRepoInfo() {
|
|
13
|
+
// Try to detect from git
|
|
14
|
+
try {
|
|
15
|
+
const { stdout: remoteUrl } = await execAsync('git remote get-url origin');
|
|
16
|
+
// Support https://github.com/owner/repo.git or git@github.com:owner/repo.git
|
|
17
|
+
let owner = '', repo = '';
|
|
18
|
+
const cleanUrl = remoteUrl.trim().replace(/\.git$/, '');
|
|
19
|
+
if (cleanUrl.startsWith('http')) {
|
|
20
|
+
const parts = cleanUrl.split('/');
|
|
21
|
+
owner = parts[parts.length - 2];
|
|
22
|
+
repo = parts[parts.length - 1];
|
|
23
|
+
}
|
|
24
|
+
else if (cleanUrl.includes(':')) {
|
|
25
|
+
const parts = cleanUrl.split(':');
|
|
26
|
+
const path = parts[1].split('/');
|
|
27
|
+
owner = path[0];
|
|
28
|
+
repo = path[1];
|
|
29
|
+
}
|
|
30
|
+
const { stdout: branch } = await execAsync('git rev-parse --abbrev-ref HEAD');
|
|
31
|
+
return { owner, repo, branch: branch.trim() };
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.warn("[JulesClient] Could not detect git repo info locally. Falling back to defaults if possible.");
|
|
35
|
+
return { owner: 'stancsz', repo: 'simple-cli', branch: 'main' }; // Fallback
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async listSources() {
|
|
39
|
+
const url = `${this.apiBaseUrl}/sources`;
|
|
40
|
+
const response = await fetch(url, {
|
|
41
|
+
headers: {
|
|
42
|
+
'X-Goog-Api-Key': this.apiKey || ''
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`Failed to list sources: ${response.status} ${response.statusText} - ${await response.text()}`);
|
|
47
|
+
}
|
|
48
|
+
const data = await response.json();
|
|
49
|
+
return data.sources || [];
|
|
50
|
+
}
|
|
51
|
+
async createSession(sourceName, prompt, branch) {
|
|
52
|
+
const url = `${this.apiBaseUrl}/sessions`;
|
|
53
|
+
const body = {
|
|
54
|
+
prompt,
|
|
55
|
+
sourceContext: {
|
|
56
|
+
source: sourceName,
|
|
57
|
+
githubRepoContext: {
|
|
58
|
+
startingBranch: branch
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
automationMode: "AUTO_CREATE_PR",
|
|
62
|
+
title: `Task: ${prompt.substring(0, 30)}...`
|
|
63
|
+
};
|
|
64
|
+
const response = await fetch(url, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
'X-Goog-Api-Key': this.apiKey || ''
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify(body)
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`Failed to create session: ${response.status} ${response.statusText} - ${await response.text()}`);
|
|
74
|
+
}
|
|
75
|
+
return await response.json();
|
|
76
|
+
}
|
|
77
|
+
async getSession(sessionId) {
|
|
78
|
+
const url = `${this.apiBaseUrl}/sessions/${sessionId}`; // Note: sessionId might include "sessions/" prefix
|
|
79
|
+
const fullUrl = sessionId.startsWith('sessions/')
|
|
80
|
+
? `${this.apiBaseUrl}/${sessionId}`
|
|
81
|
+
: `${this.apiBaseUrl}/sessions/${sessionId}`;
|
|
82
|
+
const response = await fetch(fullUrl, {
|
|
83
|
+
headers: {
|
|
84
|
+
'X-Goog-Api-Key': this.apiKey || ''
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error(`Failed to get session: ${response.status} ${response.statusText}`);
|
|
89
|
+
}
|
|
90
|
+
return await response.json();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Executes a task using the Jules API.
|
|
94
|
+
*/
|
|
95
|
+
async executeTask(task, contextFiles = []) {
|
|
96
|
+
try {
|
|
97
|
+
if (!this.apiKey) {
|
|
98
|
+
return { success: false, message: "JULES_API_KEY not set. Cannot call Jules API." };
|
|
99
|
+
}
|
|
100
|
+
console.log(`[JulesClient] Detecting repository...`);
|
|
101
|
+
const { owner, repo, branch } = await this.getRepoInfo();
|
|
102
|
+
console.log(`[JulesClient] Target: ${owner}/${repo} on branch ${branch}`);
|
|
103
|
+
console.log(`[JulesClient] Finding source in Jules...`);
|
|
104
|
+
const sources = await this.listSources();
|
|
105
|
+
const source = sources.find(s => s.githubRepo.owner === owner && s.githubRepo.repo === repo);
|
|
106
|
+
if (!source) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
message: `Repository ${owner}/${repo} not found in your Jules sources. Please connect it first.`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
console.log(`[JulesClient] Found source: ${source.name}`);
|
|
113
|
+
// Augment prompt with context files if any
|
|
114
|
+
let prompt = task;
|
|
115
|
+
if (contextFiles.length > 0) {
|
|
116
|
+
prompt += `\n\nContext Files: ${contextFiles.join(', ')} (Please read these if needed)`;
|
|
117
|
+
}
|
|
118
|
+
console.log(`[JulesClient] Creating session for task: "${task}"...`);
|
|
119
|
+
const session = await this.createSession(source.name, prompt, branch);
|
|
120
|
+
console.log(`[JulesClient] Session created: ${session.name}`);
|
|
121
|
+
// Poll for PR
|
|
122
|
+
console.log(`[JulesClient] Polling for Pull Request (timeout 5m)...`);
|
|
123
|
+
const startTime = Date.now();
|
|
124
|
+
while (Date.now() - startTime < 300000) { // 5 min timeout
|
|
125
|
+
const updatedSession = await this.getSession(session.name);
|
|
126
|
+
if (updatedSession.outputs && updatedSession.outputs.length > 0) {
|
|
127
|
+
for (const output of updatedSession.outputs) {
|
|
128
|
+
if (output.pullRequest) {
|
|
129
|
+
return {
|
|
130
|
+
success: true,
|
|
131
|
+
prUrl: output.pullRequest.url,
|
|
132
|
+
message: `Jules created PR: ${output.pullRequest.url}`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Wait 5 seconds
|
|
138
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
139
|
+
process.stdout.write('.');
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
message: "Timeout waiting for Jules to create a PR. Session is still active: " + session.name
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
console.error(`[JulesClient] Error executing task:`, error);
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
message: `Jules API Task failed: ${error.message}`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function main() {
|
|
156
|
+
const args = process.argv.slice(2);
|
|
157
|
+
const files = [];
|
|
158
|
+
let task = '';
|
|
159
|
+
// Simple arg parsing
|
|
160
|
+
for (let i = 0; i < args.length; i++) {
|
|
161
|
+
const arg = args[i];
|
|
162
|
+
if (arg === '--file' || arg === '-f') {
|
|
163
|
+
if (i + 1 < args.length) {
|
|
164
|
+
files.push(args[++i]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (!arg.startsWith('-')) {
|
|
168
|
+
// Assume the first non-flag arg is the task (or append to task)
|
|
169
|
+
if (task)
|
|
170
|
+
task += ' ' + arg;
|
|
171
|
+
else
|
|
172
|
+
task = arg;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!task) {
|
|
176
|
+
console.error('Error: No task description provided.');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
console.log(`[Jules Agent] Starting task: "${task}"`);
|
|
180
|
+
console.log(`[Jules Agent] Context files: ${files.length}`);
|
|
181
|
+
const client = new JulesClient({
|
|
182
|
+
apiKey: process.env.JULES_API_KEY,
|
|
183
|
+
});
|
|
184
|
+
// Execute the task
|
|
185
|
+
const result = await client.executeTask(task, files);
|
|
186
|
+
if (result.success) {
|
|
187
|
+
console.log(`\n[SUCCESS] Task completed.`);
|
|
188
|
+
if (result.prUrl) {
|
|
189
|
+
console.log(`PR Created: ${result.prUrl}`);
|
|
190
|
+
// JSON output for structured tools
|
|
191
|
+
console.log(JSON.stringify({ status: 'success', pr_url: result.prUrl, message: result.message }));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log(result.message);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
console.error(`\n[FAILURE] Task failed.`);
|
|
199
|
+
console.error(result.message);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
main().catch(err => {
|
|
204
|
+
console.error('Unexpected error:', err);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface JulesTaskResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
prUrl?: string;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class JulesClient {
|
|
7
|
+
private apiBaseUrl;
|
|
8
|
+
private apiKey?;
|
|
9
|
+
constructor(config?: {
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
apiBaseUrl?: string;
|
|
12
|
+
});
|
|
13
|
+
private getRepoInfo;
|
|
14
|
+
private listSources;
|
|
15
|
+
private createSession;
|
|
16
|
+
private getSession;
|
|
17
|
+
/**
|
|
18
|
+
* Executes a task using the Jules API.
|
|
19
|
+
*/
|
|
20
|
+
executeTask(task: string, contextFiles?: string[]): Promise<JulesTaskResult>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
export class JulesClient {
|
|
6
|
+
apiBaseUrl;
|
|
7
|
+
apiKey;
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.apiKey = config.apiKey || process.env.JULES_API_KEY;
|
|
10
|
+
this.apiBaseUrl = config.apiBaseUrl || 'https://jules.googleapis.com/v1alpha';
|
|
11
|
+
}
|
|
12
|
+
async getRepoInfo() {
|
|
13
|
+
// Try to detect from git
|
|
14
|
+
try {
|
|
15
|
+
const { stdout: remoteUrl } = await execAsync('git remote get-url origin');
|
|
16
|
+
// Support https://github.com/owner/repo.git or git@github.com:owner/repo.git
|
|
17
|
+
let owner = '', repo = '';
|
|
18
|
+
const cleanUrl = remoteUrl.trim().replace(/\.git$/, '');
|
|
19
|
+
if (cleanUrl.startsWith('http')) {
|
|
20
|
+
const parts = cleanUrl.split('/');
|
|
21
|
+
owner = parts[parts.length - 2];
|
|
22
|
+
repo = parts[parts.length - 1];
|
|
23
|
+
}
|
|
24
|
+
else if (cleanUrl.includes(':')) {
|
|
25
|
+
const parts = cleanUrl.split(':');
|
|
26
|
+
const path = parts[1].split('/');
|
|
27
|
+
owner = path[0];
|
|
28
|
+
repo = path[1];
|
|
29
|
+
}
|
|
30
|
+
const { stdout: branch } = await execAsync('git rev-parse --abbrev-ref HEAD');
|
|
31
|
+
return { owner, repo, branch: branch.trim() };
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.warn("[JulesClient] Could not detect git repo info locally. Falling back to defaults if possible.");
|
|
35
|
+
return { owner: 'stancsz', repo: 'simple-cli', branch: 'main' }; // Fallback
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async listSources() {
|
|
39
|
+
const url = `${this.apiBaseUrl}/sources`;
|
|
40
|
+
const response = await fetch(url, {
|
|
41
|
+
headers: {
|
|
42
|
+
'X-Goog-Api-Key': this.apiKey || ''
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`Failed to list sources: ${response.status} ${response.statusText} - ${await response.text()}`);
|
|
47
|
+
}
|
|
48
|
+
const data = await response.json();
|
|
49
|
+
return data.sources || [];
|
|
50
|
+
}
|
|
51
|
+
async createSession(sourceName, prompt, branch) {
|
|
52
|
+
const url = `${this.apiBaseUrl}/sessions`;
|
|
53
|
+
const body = {
|
|
54
|
+
prompt,
|
|
55
|
+
sourceContext: {
|
|
56
|
+
source: sourceName,
|
|
57
|
+
githubRepoContext: {
|
|
58
|
+
startingBranch: branch
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
automationMode: "AUTO_CREATE_PR",
|
|
62
|
+
title: `Task: ${prompt.substring(0, 30)}...`
|
|
63
|
+
};
|
|
64
|
+
const response = await fetch(url, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
'X-Goog-Api-Key': this.apiKey || ''
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify(body)
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`Failed to create session: ${response.status} ${response.statusText} - ${await response.text()}`);
|
|
74
|
+
}
|
|
75
|
+
return await response.json();
|
|
76
|
+
}
|
|
77
|
+
async getSession(sessionId) {
|
|
78
|
+
const url = `${this.apiBaseUrl}/sessions/${sessionId}`; // Note: sessionId might include "sessions/" prefix
|
|
79
|
+
// Fix: The ID in API might be numeric or "sessions/numeric". Ensure we handle URL construction correctly.
|
|
80
|
+
// If sessionId is "sessions/123", appending it to base is tricky if base is .../v1alpha.
|
|
81
|
+
// The API returns "name": "sessions/..." which is the resource name.
|
|
82
|
+
// We should likely request `${this.apiBaseUrl}/${resourceName}` if resourceName contains the type.
|
|
83
|
+
const fullUrl = sessionId.startsWith('sessions/')
|
|
84
|
+
? `${this.apiBaseUrl}/${sessionId}`
|
|
85
|
+
: `${this.apiBaseUrl}/sessions/${sessionId}`;
|
|
86
|
+
const response = await fetch(fullUrl, {
|
|
87
|
+
headers: {
|
|
88
|
+
'X-Goog-Api-Key': this.apiKey || ''
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(`Failed to get session: ${response.status} ${response.statusText}`);
|
|
93
|
+
}
|
|
94
|
+
return await response.json();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Executes a task using the Jules API.
|
|
98
|
+
*/
|
|
99
|
+
async executeTask(task, contextFiles = []) {
|
|
100
|
+
try {
|
|
101
|
+
if (!this.apiKey) {
|
|
102
|
+
return { success: false, message: "JULES_API_KEY not set. Cannot call Jules API." };
|
|
103
|
+
}
|
|
104
|
+
console.log(`[JulesClient] Detecting repository...`);
|
|
105
|
+
const { owner, repo, branch } = await this.getRepoInfo();
|
|
106
|
+
console.log(`[JulesClient] Target: ${owner}/${repo} on branch ${branch}`);
|
|
107
|
+
console.log(`[JulesClient] Finding source in Jules...`);
|
|
108
|
+
const sources = await this.listSources();
|
|
109
|
+
const source = sources.find(s => s.githubRepo.owner === owner && s.githubRepo.repo === repo);
|
|
110
|
+
if (!source) {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
message: `Repository ${owner}/${repo} not found in your Jules sources. Please connect it first.`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
console.log(`[JulesClient] Found source: ${source.name}`);
|
|
117
|
+
// Augment prompt with context files if any
|
|
118
|
+
let prompt = task;
|
|
119
|
+
if (contextFiles.length > 0) {
|
|
120
|
+
prompt += `\n\nContext Files: ${contextFiles.join(', ')} (Please read these if needed)`;
|
|
121
|
+
}
|
|
122
|
+
console.log(`[JulesClient] Creating session for task: "${task}"...`);
|
|
123
|
+
const session = await this.createSession(source.name, prompt, branch);
|
|
124
|
+
console.log(`[JulesClient] Session created: ${session.name}`);
|
|
125
|
+
// Poll for PR
|
|
126
|
+
console.log(`[JulesClient] Polling for Pull Request (timeout 5m)...`);
|
|
127
|
+
const startTime = Date.now();
|
|
128
|
+
while (Date.now() - startTime < 300000) { // 5 min timeout
|
|
129
|
+
const updatedSession = await this.getSession(session.name);
|
|
130
|
+
if (updatedSession.outputs && updatedSession.outputs.length > 0) {
|
|
131
|
+
for (const output of updatedSession.outputs) {
|
|
132
|
+
if (output.pullRequest) {
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
prUrl: output.pullRequest.url,
|
|
136
|
+
message: `Jules created PR: ${output.pullRequest.url}`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Wait 5 seconds
|
|
142
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
143
|
+
process.stdout.write('.');
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
message: "Timeout waiting for Jules to create a PR. Session is still active: " + session.name
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.error(`[JulesClient] Error executing task:`, error);
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
message: `Jules API Task failed: ${error.message}`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
package/dist/anyllm.py
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface AsyncTask {
|
|
2
|
+
id: string;
|
|
3
|
+
command: string;
|
|
4
|
+
startTime: number;
|
|
5
|
+
pid: number;
|
|
6
|
+
logFile: string;
|
|
7
|
+
status: 'running' | 'completed' | 'failed' | 'unknown';
|
|
8
|
+
exitCode?: number | null;
|
|
9
|
+
}
|
|
10
|
+
export declare class AsyncTaskManager {
|
|
11
|
+
private static instance;
|
|
12
|
+
private tasksDir;
|
|
13
|
+
private constructor();
|
|
14
|
+
static getInstance(cwd?: string): AsyncTaskManager;
|
|
15
|
+
init(): Promise<void>;
|
|
16
|
+
startTask(command: string, args: string[], env?: NodeJS.ProcessEnv): Promise<string>;
|
|
17
|
+
getTaskStatus(id: string): Promise<AsyncTask>;
|
|
18
|
+
getTaskLogs(id: string, lines?: number): Promise<string>;
|
|
19
|
+
listTasks(): Promise<AsyncTask[]>;
|
|
20
|
+
}
|