cowork-cli 0.0.1 β 1.0.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 +78 -0
- package/bin/cli.js +43 -0
- package/package.json +33 -5
- package/src/configs/config.json +11 -0
- package/src/configs/sys.txt +33 -0
- package/src/engine/client.js +26 -0
- package/src/engine/models/BaseModel.js +228 -0
- package/src/engine/models/default.js +8 -0
- package/src/engine/models/gemini.js +20 -0
- package/src/engine/run.js +50 -0
- package/src/engine/tools/askConfirm.js +34 -0
- package/src/engine/tools/askUser.js +32 -0
- package/src/engine/tools/findDir.js +73 -0
- package/src/engine/tools/findFile.js +74 -0
- package/src/engine/tools/index.js +204 -0
- package/src/engine/tools/listTools.js +79 -0
- package/src/engine/tools/projectTree.js +89 -0
- package/src/engine/tools/readDir.js +32 -0
- package/src/engine/tools/readFile.js +41 -0
- package/src/engine/tools/readFileChunk.js +48 -0
- package/src/engine/tools/searchText.js +133 -0
- package/src/engine/tools/webFetch.js +154 -0
- package/src/main.js +50 -0
- package/src/utils/configManager.js +71 -0
- package/src/utils/fsUtils.js +37 -0
- package/src/utils/helpMsg.js +23 -0
- package/src/utils/logger.js +56 -0
- package/src/utils/outputFormatter.js +72 -0
- package/src/utils/ui.js +582 -0
- package/index.js +0 -2
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# π cowork-cli (cwk)
|
|
2
|
+
|
|
3
|
+
**Stop waiting. Start knowing.**
|
|
4
|
+
|
|
5
|
+
`cowork-cli` (`cwk`) is the ultimate high-speed CLI Analyst for developers who need answers, not a conversation. It's a minimalist, context-aware co-processor that lives in your terminal and understands your code as well as you do.
|
|
6
|
+
|
|
7
|
+
## π₯ Universal AI Power
|
|
8
|
+
|
|
9
|
+
Think your favorite model won't work? **Think again!** `cwk` is built to be compatible with **any** (yes, ANY!) OpenAI-compatible API endpoint on the planet.
|
|
10
|
+
|
|
11
|
+
Whether you're running local models via Ollama, using specialized providers, or tapping into the world's most powerful LLMs via **OpenRouter**, `cwk` has you covered.
|
|
12
|
+
|
|
13
|
+
### π Full Gemini Support
|
|
14
|
+
Love Google's Gemini models? We do too. `cwk` features a specialized handler to preserve the high-density analytical capabilities of the Gemini suite, ensuring you get the best out of **Gemini 3.1 Pro** and **Flash**.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## π οΈ Rapid Setup (OpenRouter Example)
|
|
19
|
+
|
|
20
|
+
Get up and running in seconds. Just point `cwk` to your provider in your `~/.env` file:
|
|
21
|
+
|
|
22
|
+
```env
|
|
23
|
+
# Example using OpenRouter to access Gemini 3.1 Pro
|
|
24
|
+
CWK_MODEL_NAME=google/gemini-3.1-pro
|
|
25
|
+
CWK_MODEL_URL=https://openrouter.ai/api/v1
|
|
26
|
+
CWK_MODEL_API_KEY=your_openrouter_key
|
|
27
|
+
CWK_MODEL_TYPE=openai
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## β‘ Real-World Usage
|
|
33
|
+
|
|
34
|
+
`cwk` doesn't just "chat"βit **investigates**. It uses a suite of built-in tools to map your project, search for patterns, and read files before giving you a hard-hitting, plain-text technical answer.
|
|
35
|
+
|
|
36
|
+
### π Explore your codebase
|
|
37
|
+
```bash
|
|
38
|
+
cwk "Where is the authentication logic handled?"
|
|
39
|
+
```
|
|
40
|
+
*`cwk` will automatically list directories, find relevant files, and peek at the code to give you a precise summary.*
|
|
41
|
+
|
|
42
|
+
### π οΈ Debug like a pro
|
|
43
|
+
```bash
|
|
44
|
+
cwk "Find all 'FIXME' tags in src/ and tell me which one is most critical"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### π§ Instant Documentation
|
|
48
|
+
```bash
|
|
49
|
+
cwk "Explain the data flow in the engine/ models"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## β¨ Features that Matter
|
|
55
|
+
|
|
56
|
+
- **Zero-Whitespace UI**: High-density terminal output designed for professionals. No fluff, no headers, just data.
|
|
57
|
+
- **Interactive Feedback**: The AI can request clarifications via the `askUser` tool or trigger an interactive `[ Yes ] No` toggle using `askConfirm`.
|
|
58
|
+
- **Smart Discovery**: Built-in `searchText`, `findFile`, and `projectTree` tools that respect your `.gitignore`.
|
|
59
|
+
- **Surgical I/O**: Read entire files or specific line ranges (`readFileChunk`) with automatic binary detection.
|
|
60
|
+
- **Piping Support**: Pipe logs or diffs directly into `cwk` for instant analysis.
|
|
61
|
+
|
|
62
|
+
## π¦ Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm install -g cowork-cli
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## β¨οΈ Commands
|
|
69
|
+
|
|
70
|
+
| Command | Description |
|
|
71
|
+
| :--- | :--- |
|
|
72
|
+
| `cwk "query"` | Run a one-shot analysis on your codebase. |
|
|
73
|
+
| `cwk -v`, `--version` | Display the current version of `cwk`. |
|
|
74
|
+
| `cwk --help` | Show the minimalist help menu. |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
*βcwk... how does this work again?β*
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file cli.js
|
|
5
|
+
* @description Executable entry point for the cwk CLI tool.
|
|
6
|
+
* Handles top-level error boundaries and passes execution to the main application logic.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import main from "../src/main.js";
|
|
10
|
+
import { logger } from "../src/utils/logger.js";
|
|
11
|
+
|
|
12
|
+
// Catch unhandled promise rejections
|
|
13
|
+
process.on('unhandledRejection', (reason) => {
|
|
14
|
+
process.stdout.write('\x1b[?25h'); // Restore cursor
|
|
15
|
+
logger.error(`[Fatal] Unhandled Rejection: ${reason instanceof Error ? reason.message : reason}`);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Catch uncaught exceptions
|
|
20
|
+
process.on('uncaughtException', (err) => {
|
|
21
|
+
process.stdout.write('\x1b[?25h'); // Restore cursor
|
|
22
|
+
logger.error(`[Fatal] Uncaught Exception: ${err.message}`);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Graceful exit on interrupt
|
|
27
|
+
process.on('SIGINT', () => {
|
|
28
|
+
process.stdout.write('\x1b[?25h'); // Restore cursor
|
|
29
|
+
logger.secondary('Process interrupted by user.');
|
|
30
|
+
process.exit(130);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
async function run() {
|
|
34
|
+
try {
|
|
35
|
+
// Pass command line arguments (excluding 'node' and the script path) to main
|
|
36
|
+
await main(process.argv.slice(2));
|
|
37
|
+
} catch (err) {
|
|
38
|
+
logger.error(`[Error]: ${err.message}`);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
run();
|
package/package.json
CHANGED
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cowork-cli",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"type": "module",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "work with cowork",
|
|
6
5
|
"bin": {
|
|
7
|
-
"cwk": "
|
|
6
|
+
"cwk": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"src/",
|
|
11
|
+
"README.md",
|
|
12
|
+
"package.json"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"cli",
|
|
16
|
+
"ai"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/sapirrior/cowork-cli#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/sapirrior/cowork-cli/issues"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/sapirrior/cowork-cli.git"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "nolan stark",
|
|
28
|
+
"type": "module",
|
|
29
|
+
"main": "./src/main.js",
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
32
|
},
|
|
9
|
-
"
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"dotenv": "^17.4.2",
|
|
35
|
+
"ipaddr.js": "^2.4.0",
|
|
36
|
+
"openai": "^6.38.0"
|
|
37
|
+
}
|
|
10
38
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# IDENTITY
|
|
2
|
+
You are Cowork (cwk), an interactive CLI tool acting as a Full Engineering Read-Only Analyst.
|
|
3
|
+
|
|
4
|
+
# CORE MANDATES
|
|
5
|
+
- ONLY perform exactly what the user explicitly says.
|
|
6
|
+
- NO unsolicited investigations, side tasks, or extra work of any kind.
|
|
7
|
+
- Provide accurate data with absolute minimum token usage unless "deep work" is requested.
|
|
8
|
+
|
|
9
|
+
# CONSTRAINTS (Terminal Environment)
|
|
10
|
+
- NO conversational filler, pleasantries, preambles, or postambles.
|
|
11
|
+
- NO Markdown (no *, `, #, or code blocks). NO XML/HTML tags.
|
|
12
|
+
- Output ONLY structured, concise plain text with simple spacing/indentation.
|
|
13
|
+
- Cite code locations using: file_path:line_number.
|
|
14
|
+
|
|
15
|
+
# EXECUTION & STYLE
|
|
16
|
+
- Answer directly without elaboration or explanation.
|
|
17
|
+
- Use fewer than 4 lines (excluding tool use). One-word answers are preferred.
|
|
18
|
+
- Use all tools efficiently (parallel/sequential) to minimize context usage.
|
|
19
|
+
- Verify dependencies and mimic local code style; never assume availability.
|
|
20
|
+
|
|
21
|
+
# EXAMPLES
|
|
22
|
+
user: files in src/?
|
|
23
|
+
assistant:
|
|
24
|
+
src/foo.js
|
|
25
|
+
src/bar.js
|
|
26
|
+
|
|
27
|
+
user: where is foo?
|
|
28
|
+
assistant:
|
|
29
|
+
src/foo.js:12
|
|
30
|
+
|
|
31
|
+
# CONTEXT
|
|
32
|
+
Working directory: ${folder}
|
|
33
|
+
Year: ${year}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { OpenAI } from 'openai';
|
|
2
|
+
import { loadConfig, validateConfig } from '../utils/configManager.js';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initializes and returns an OpenAI client instance.
|
|
7
|
+
* @returns {OpenAI} An instance of the OpenAI client.
|
|
8
|
+
*/
|
|
9
|
+
function clientLoader() {
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
|
|
12
|
+
if (!validateConfig(config)) {
|
|
13
|
+
throw new Error("Configuration missing or invalid. Please configure your ~/.env file (run 'cwk --help' for details).");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Normalize baseURL: remove trailing slashes as the SDK appends paths starting with /
|
|
17
|
+
const baseURL = config.model_url.replace(/\/+$/, '');
|
|
18
|
+
|
|
19
|
+
return new OpenAI({
|
|
20
|
+
apiKey: config.model_api_key,
|
|
21
|
+
baseURL: baseURL,
|
|
22
|
+
timeout: 60000 // 60 seconds timeout
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default clientLoader;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { toolDefinitions, dispatchTool } from '../tools/index.js';
|
|
2
|
+
import { logger } from '../../utils/logger.js';
|
|
3
|
+
import { ui } from '../../utils/ui.js';
|
|
4
|
+
import { outputFormatted } from '../../utils/outputFormatter.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base class for AI model interaction handlers.
|
|
8
|
+
* Encapsulates message history, API calling with retries, and robust tool execution.
|
|
9
|
+
*/
|
|
10
|
+
export default class BaseModel {
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('openai').OpenAI} client Initialized OpenAI client.
|
|
13
|
+
* @param {string} model Model identifier.
|
|
14
|
+
*/
|
|
15
|
+
constructor(client, model) {
|
|
16
|
+
this.client = client;
|
|
17
|
+
this.model = model;
|
|
18
|
+
this.messages = [];
|
|
19
|
+
this.maxTurns = 15; // Safeguard against infinite tool-calling loops
|
|
20
|
+
this.lastRequestTime = 0; // For proactive throttling
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Adds a message to the conversation history.
|
|
25
|
+
* @param {string} role 'user', 'assistant', 'system', or 'tool'.
|
|
26
|
+
* @param {string} content Message content.
|
|
27
|
+
* @param {Object} extra Additional fields (e.g., tool_call_id).
|
|
28
|
+
*/
|
|
29
|
+
addMessage(role, content, extra = {}) {
|
|
30
|
+
this.messages.push({ role, content, ...extra });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Main execution loop for the model query.
|
|
35
|
+
* @param {string} query The user input.
|
|
36
|
+
* @param {string|null} systemPrompt Optional system-level instructions.
|
|
37
|
+
*/
|
|
38
|
+
async run(query, systemPrompt = null) {
|
|
39
|
+
if (systemPrompt) {
|
|
40
|
+
this.addMessage('system', systemPrompt);
|
|
41
|
+
}
|
|
42
|
+
this.addMessage('user', query);
|
|
43
|
+
|
|
44
|
+
let turn = 0;
|
|
45
|
+
while (turn < this.maxTurns) {
|
|
46
|
+
turn++;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
ui.think();
|
|
50
|
+
const response = await this._getCompletion();
|
|
51
|
+
ui.stop();
|
|
52
|
+
|
|
53
|
+
const message = response.choices[0].message;
|
|
54
|
+
|
|
55
|
+
// Let subclasses handle/format the response (e.g. Gemini thought signatures)
|
|
56
|
+
await this.handleResponse(message);
|
|
57
|
+
|
|
58
|
+
// Exit loop if no tool calls are requested (Final Answer)
|
|
59
|
+
if (!message.tool_calls || message.tool_calls.length === 0) {
|
|
60
|
+
if (message.content) {
|
|
61
|
+
const formatted = outputFormatted(message.content);
|
|
62
|
+
process.stdout.write(formatted);
|
|
63
|
+
if (!formatted.endsWith('\n')) {
|
|
64
|
+
process.stdout.write("\n");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Execute and record tool calls
|
|
71
|
+
await this._processToolCalls(message.tool_calls);
|
|
72
|
+
|
|
73
|
+
} catch (err) {
|
|
74
|
+
ui.stop();
|
|
75
|
+
// Deep error logging for API failures
|
|
76
|
+
if (err.status) {
|
|
77
|
+
logger.error(`[API Error] Status: ${err.status}`);
|
|
78
|
+
if (err.response?.data) {
|
|
79
|
+
logger.error(`[API Error] Details: ${JSON.stringify(err.response.data)}`);
|
|
80
|
+
}
|
|
81
|
+
} else if (err.name === 'AbortError' || err.message.includes('timeout')) {
|
|
82
|
+
logger.error(`[Timeout Error]: The AI took too long to respond (60s).`);
|
|
83
|
+
} else {
|
|
84
|
+
logger.error(`[Error]: ${err.message}`);
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
logger.secondary("[System]: Reached maximum conversation turns. Ending session.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Private method to fetch completion with exponential backoff for transient errors.
|
|
95
|
+
* @private
|
|
96
|
+
*/
|
|
97
|
+
async _getCompletion() {
|
|
98
|
+
let retries = 0;
|
|
99
|
+
const maxRetries = 5;
|
|
100
|
+
const minDelayBetweenRequests = 1000; // 1s proactive throttle
|
|
101
|
+
|
|
102
|
+
while (retries <= maxRetries) {
|
|
103
|
+
try {
|
|
104
|
+
// 1. Proactive Throttling
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
107
|
+
if (timeSinceLastRequest < minDelayBetweenRequests) {
|
|
108
|
+
const waitTime = minDelayBetweenRequests - timeSinceLastRequest;
|
|
109
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const response = await this.client.chat.completions.create({
|
|
113
|
+
model: this.model,
|
|
114
|
+
messages: this.messages,
|
|
115
|
+
tools: toolDefinitions,
|
|
116
|
+
tool_choice: "auto"
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Update last request time on successful response
|
|
120
|
+
this.lastRequestTime = Date.now();
|
|
121
|
+
return response;
|
|
122
|
+
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const isTransient = [429, 500, 502, 503, 504].includes(err.status);
|
|
125
|
+
if (isTransient && retries < maxRetries) {
|
|
126
|
+
retries++;
|
|
127
|
+
|
|
128
|
+
let delay = Math.pow(2, retries) * 1000;
|
|
129
|
+
|
|
130
|
+
// 2. Adhere to Retry-After header if present
|
|
131
|
+
const retryAfter = err.headers?.['retry-after'];
|
|
132
|
+
if (retryAfter) {
|
|
133
|
+
const seconds = parseInt(retryAfter);
|
|
134
|
+
if (!isNaN(seconds)) {
|
|
135
|
+
delay = seconds * 1000;
|
|
136
|
+
} else {
|
|
137
|
+
// Handle Date string
|
|
138
|
+
const retryDate = new Date(retryAfter);
|
|
139
|
+
if (!isNaN(retryDate.getTime())) {
|
|
140
|
+
delay = Math.max(0, retryDate.getTime() - Date.now());
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 3. Apply Jitter (randomness to prevent thundering herd)
|
|
146
|
+
const jitter = Math.random() * 500;
|
|
147
|
+
const finalDelay = delay + jitter;
|
|
148
|
+
|
|
149
|
+
ui.update(`Error ${err.status}. Retrying in ${(finalDelay/1000).toFixed(1)}s`);
|
|
150
|
+
await new Promise(resolve => setTimeout(resolve, finalDelay));
|
|
151
|
+
ui.update('Thinking');
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Private method to handle tool calls with deep error recovery.
|
|
161
|
+
* @private
|
|
162
|
+
*/
|
|
163
|
+
async _processToolCalls(toolCalls) {
|
|
164
|
+
for (const toolCall of toolCalls) {
|
|
165
|
+
const name = toolCall.function.name;
|
|
166
|
+
let args;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
// 1. Robust Argument Parsing
|
|
170
|
+
try {
|
|
171
|
+
args = JSON.parse(toolCall.function.arguments);
|
|
172
|
+
} catch (parseErr) {
|
|
173
|
+
throw new Error(`Invalid JSON arguments provided for tool '${name}': ${parseErr.message}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Semantic Tool Logging
|
|
177
|
+
const toolLabels = {
|
|
178
|
+
readFile: 'reading',
|
|
179
|
+
readDir: 'listing',
|
|
180
|
+
projectTree: 'mapping',
|
|
181
|
+
readFileChunk: 'peeking',
|
|
182
|
+
searchText: 'searching',
|
|
183
|
+
webFetch: 'fetching',
|
|
184
|
+
findFile: 'finding',
|
|
185
|
+
findDir: 'finding',
|
|
186
|
+
listTools: 'listing'
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const label = toolLabels[name] || name;
|
|
190
|
+
let displayArg = "";
|
|
191
|
+
|
|
192
|
+
if (name === 'searchText') displayArg = `'${args.pattern}' in ${args.path}`;
|
|
193
|
+
else if (name === 'findFile' || name === 'findDir') displayArg = `'${args.pattern}' in ${args.dirPath || '.'}`;
|
|
194
|
+
else if (name === 'readFileChunk') displayArg = `${args.filePath} [L${args.startLine}-${args.endLine}]`;
|
|
195
|
+
else displayArg = args.url || args.filePath || args.dirPath || args.path || args.pattern || JSON.stringify(args);
|
|
196
|
+
|
|
197
|
+
// ui.start() handles terminal-aware truncation internally.
|
|
198
|
+
if (name !== 'askUser' && name !== 'askConfirm') {
|
|
199
|
+
ui.start(label, displayArg);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const result = await dispatchTool(name, args);
|
|
203
|
+
|
|
204
|
+
if (name !== 'askUser' && name !== 'askConfirm') {
|
|
205
|
+
ui.stop();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.addMessage('tool', result, { tool_call_id: toolCall.id });
|
|
209
|
+
|
|
210
|
+
} catch (err) {
|
|
211
|
+
ui.stop();
|
|
212
|
+
const errorMsg = err.message;
|
|
213
|
+
logger.error(`[FAILED] ${name}: ${errorMsg}`);
|
|
214
|
+
|
|
215
|
+
// 3. Model Recovery: Feed the error back to the model
|
|
216
|
+
this.addMessage('tool', `Error: ${errorMsg}`, { tool_call_id: toolCall.id });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Overridden by subclasses to handle provider-specific message formatting.
|
|
223
|
+
* @param {Object} message The message object from the API.
|
|
224
|
+
*/
|
|
225
|
+
async handleResponse(message) {
|
|
226
|
+
this.messages.push(message);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import BaseModel from './BaseModel.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gemini-specific model handler.
|
|
5
|
+
* Handles preservation of 'thought_signature' in tool calls.
|
|
6
|
+
*/
|
|
7
|
+
export default class GeminiModel extends BaseModel {
|
|
8
|
+
/**
|
|
9
|
+
* Gemini requires the 'thought_signature' and potentially other metadata
|
|
10
|
+
* to be passed back in the conversation history for tool-calling turns.
|
|
11
|
+
*/
|
|
12
|
+
async handleResponse(message) {
|
|
13
|
+
// We push the full message object to ensure all provider-specific
|
|
14
|
+
// fields (like thought_signature) are preserved in the history.
|
|
15
|
+
this.messages.push(message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// We might need to override run to handle the 'extra_body' if the SDK is too strict,
|
|
19
|
+
// but let's try the simple history preservation first.
|
|
20
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import DefaultModel from './models/default.js';
|
|
6
|
+
import GeminiModel from './models/gemini.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Executes a chat completion query using the appropriate model handler.
|
|
12
|
+
* @param {import('openai').OpenAI} client The initialized OpenAI client.
|
|
13
|
+
* @param {Object} config The user configuration (from .env).
|
|
14
|
+
* @param {string} query The user query string.
|
|
15
|
+
*/
|
|
16
|
+
export default async function runQuery(client, config, query) {
|
|
17
|
+
if (!query) {
|
|
18
|
+
logger.error("Error: No query provided.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// 1. Load and format system prompt from internal sys.txt
|
|
24
|
+
let systemPrompt = null;
|
|
25
|
+
try {
|
|
26
|
+
const promptPath = path.join(__dirname, '../configs/sys.txt');
|
|
27
|
+
if (fs.existsSync(promptPath)) {
|
|
28
|
+
systemPrompt = fs.readFileSync(promptPath, 'utf8')
|
|
29
|
+
.replace('${folder}', process.cwd())
|
|
30
|
+
.replace(/\${year}/g, new Date().getFullYear());
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
// Fallback if prompt is missing - proceed without system prompt
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isGemini = config.model_type.toLowerCase() === 'gemini';
|
|
37
|
+
const modelHandler = isGemini
|
|
38
|
+
? new GeminiModel(client, config.model_name)
|
|
39
|
+
: new DefaultModel(client, config.model_name);
|
|
40
|
+
|
|
41
|
+
await modelHandler.run(query, systemPrompt);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
logger.error(`Error during AI execution: ${err.message}`);
|
|
44
|
+
if (err.status === 401) {
|
|
45
|
+
logger.secondary("Tip: Check if your API key is correct in your ~/.env file.");
|
|
46
|
+
} else if (err.status === 404) {
|
|
47
|
+
logger.secondary("Tip: The specified model or base URL might be incorrect.");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ui } from '../../utils/ui.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* askConfirm tool β asks the user a yes/no question via the terminal.
|
|
5
|
+
*
|
|
6
|
+
* Accepted inputs (case-insensitive):
|
|
7
|
+
* yes β y, yes
|
|
8
|
+
* no β n, no
|
|
9
|
+
* cancel β Ctrl+C (treated as no, with dismissed flag)
|
|
10
|
+
*
|
|
11
|
+
* Any other input is silently erased and the prompt re-appears.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} args
|
|
14
|
+
* @param {string} args.question The yes/no question to ask.
|
|
15
|
+
* @returns {Promise<string>} JSON: { confirmed } or { confirmed, dismissed }
|
|
16
|
+
*/
|
|
17
|
+
export default async function askConfirm({ question }) {
|
|
18
|
+
// 1. Input validation
|
|
19
|
+
if (!question || typeof question !== 'string' || question.trim().length === 0) {
|
|
20
|
+
return JSON.stringify({ error: "'question' must be a non-empty string.", dismissed: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 2. TTY guard β cannot prompt in pipes or CI environments
|
|
24
|
+
if (!process.stdin.isTTY) {
|
|
25
|
+
return JSON.stringify({
|
|
26
|
+
error: 'stdin is not a TTY. Cannot prompt in non-interactive environments.',
|
|
27
|
+
dismissed: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 3. Delegate entirely to UIEngine β it owns all terminal state
|
|
32
|
+
const result = await ui.confirm(question);
|
|
33
|
+
return JSON.stringify(result);
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ui } from '../../utils/ui.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Implementation of the askUser tool.
|
|
5
|
+
* Delegates rendering and input handling to the UIEngine singleton.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} args
|
|
8
|
+
* @param {string} args.question The question to ask the user.
|
|
9
|
+
* @returns {Promise<string>} JSON string: { answer } or { error, dismissed }.
|
|
10
|
+
*/
|
|
11
|
+
export default async function askUser({ question }) {
|
|
12
|
+
// 1. Input validation
|
|
13
|
+
if (!question || typeof question !== 'string' || question.trim().length === 0) {
|
|
14
|
+
return JSON.stringify({ error: "'question' must be a non-empty string.", dismissed: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 2. TTY check
|
|
18
|
+
if (!process.stdin.isTTY) {
|
|
19
|
+
return JSON.stringify({ error: 'stdin is not a TTY. Cannot prompt in non-interactive environments.', dismissed: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const answer = await ui.ask(question);
|
|
24
|
+
return JSON.stringify({ answer });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
// ui.ask() rejects with { cancelled: true } on SIGINT, or an Error otherwise.
|
|
27
|
+
if (err && err.cancelled) {
|
|
28
|
+
return JSON.stringify({ error: 'User cancelled the request.', dismissed: true });
|
|
29
|
+
}
|
|
30
|
+
return JSON.stringify({ error: `System Error: ${err.message}`, dismissed: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getIgnorePatterns, shouldIgnore } from '../../utils/fsUtils.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* findDir tool: Finds directories by name using regex.
|
|
7
|
+
* @param {Object} args
|
|
8
|
+
* @param {string} args.pattern Regex pattern to match directory names.
|
|
9
|
+
* @param {string} args.dirPath Root directory to search (default: current directory).
|
|
10
|
+
* @param {boolean} args.recursive Whether to search subdirectories (default: true).
|
|
11
|
+
* @param {number} args.limit Maximum number of results (default: 15, max: 15).
|
|
12
|
+
*/
|
|
13
|
+
export default async function findDir({ pattern, dirPath = '.', recursive = true, limit = 15 }) {
|
|
14
|
+
try {
|
|
15
|
+
if (!pattern) return "Error: Search pattern cannot be empty.";
|
|
16
|
+
|
|
17
|
+
// Enforce max limit of 15
|
|
18
|
+
const finalLimit = Math.min(limit, 15);
|
|
19
|
+
|
|
20
|
+
let regex;
|
|
21
|
+
try {
|
|
22
|
+
regex = new RegExp(pattern, 'i');
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return `Error: Invalid regex pattern '${pattern}': ${e.message}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ignoreList = await getIgnorePatterns();
|
|
28
|
+
const results = [];
|
|
29
|
+
|
|
30
|
+
async function walk(currentPath) {
|
|
31
|
+
if (results.length >= finalLimit) return;
|
|
32
|
+
|
|
33
|
+
let items;
|
|
34
|
+
try {
|
|
35
|
+
items = await fs.readdir(currentPath, { withFileTypes: true });
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return; // Skip unreadable directories
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
if (results.length >= finalLimit) break;
|
|
42
|
+
if (shouldIgnore(item.name, ignoreList)) continue;
|
|
43
|
+
|
|
44
|
+
const fullPath = path.join(currentPath, item.name);
|
|
45
|
+
|
|
46
|
+
if (item.isDirectory()) {
|
|
47
|
+
if (regex.test(item.name)) {
|
|
48
|
+
results.push(path.relative(process.cwd(), fullPath));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (recursive) {
|
|
52
|
+
await walk(fullPath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await walk(dirPath);
|
|
59
|
+
|
|
60
|
+
if (results.length === 0) {
|
|
61
|
+
return `No directories found matching "${pattern}" in "${dirPath}".`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let output = results.join('\n');
|
|
65
|
+
if (results.length >= finalLimit) {
|
|
66
|
+
output += `\n[Warning: Truncated at ${finalLimit} matches]`;
|
|
67
|
+
}
|
|
68
|
+
return output;
|
|
69
|
+
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return `Error searching for directories: ${err.message}`;
|
|
72
|
+
}
|
|
73
|
+
}
|