@zds-ai/cli 0.1.2 → 0.1.3
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 +62 -42
- package/dist/agent/grok-agent.d.ts +4 -4
- package/dist/agent/grok-agent.js +80 -96
- package/dist/agent/grok-agent.js.map +1 -1
- package/dist/bin/read_xlsx.py +308 -0
- package/dist/grok/client.d.ts +4 -0
- package/dist/grok/client.js +26 -6
- package/dist/grok/client.js.map +1 -1
- package/dist/hooks/use-input-handler.js +6 -4
- package/dist/hooks/use-input-handler.js.map +1 -1
- package/dist/index.js +47 -14
- package/dist/index.js.map +1 -1
- package/dist/tools/file-conversion-tool.js +11 -4
- package/dist/tools/file-conversion-tool.js.map +1 -1
- package/dist/tools/image-tool.d.ts +2 -1
- package/dist/tools/image-tool.js +12 -3
- package/dist/tools/image-tool.js.map +1 -1
- package/dist/tools/search.d.ts +1 -1
- package/dist/tools/search.js +41 -53
- package/dist/tools/search.js.map +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A conversational AI CLI tool powered by Grok with intelligent text editor capabilities and tool usage.
|
|
4
4
|
|
|
5
|
-
<img width="
|
|
5
|
+
<img width="720" height="528" alt="Image" src="https://github.com/user-attachments/assets/f697a273-141e-4f02-8c15-37143aa7ec0e" />
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -22,6 +22,26 @@ A conversational AI CLI tool powered by Grok with intelligent text editor capabi
|
|
|
22
22
|
- GROK API key from X.AI
|
|
23
23
|
- (Optional, Recommended) Morph API key for Fast Apply editing
|
|
24
24
|
|
|
25
|
+
### System Dependencies
|
|
26
|
+
|
|
27
|
+
zai-cli requires the following system tools for certain features:
|
|
28
|
+
|
|
29
|
+
- **ripgrep** (required for search functionality)
|
|
30
|
+
- macOS: `brew install ripgrep`
|
|
31
|
+
- Ubuntu/Debian: `apt install ripgrep`
|
|
32
|
+
- Windows: `choco install ripgrep` or download from [releases](https://github.com/BurntSushi/ripgrep/releases)
|
|
33
|
+
- Other platforms: See [ripgrep installation guide](https://github.com/BurntSushi/ripgrep#installation)
|
|
34
|
+
|
|
35
|
+
- **Python 3 with openpyxl** (optional, required for XLSX file reading)
|
|
36
|
+
- Install: `pip3 install openpyxl` or `python3 -m pip install openpyxl`
|
|
37
|
+
- Most systems already have Python 3 installed
|
|
38
|
+
|
|
39
|
+
- **exiftool** (optional, required for PNG metadata extraction)
|
|
40
|
+
- macOS: `brew install exiftool`
|
|
41
|
+
- Ubuntu/Debian: `apt install libimage-exiftool-perl`
|
|
42
|
+
- Windows: Download from [exiftool.org](https://exiftool.org/)
|
|
43
|
+
- Other platforms: See [exiftool installation guide](https://exiftool.org/install.html)
|
|
44
|
+
|
|
25
45
|
### Global Installation (Recommended)
|
|
26
46
|
|
|
27
47
|
```sh
|
|
@@ -49,52 +69,52 @@ bun link
|
|
|
49
69
|
1. Get your GROK API key from [X.AI](https://x.ai)
|
|
50
70
|
|
|
51
71
|
2. Set up your API key (choose one method):
|
|
52
|
-
|
|
53
|
-
**Method 1: Environment Variable**
|
|
54
|
-
|
|
55
|
-
```sh
|
|
56
|
-
export GROK_API_KEY=your_api_key_here
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
**Method 2: .env File**
|
|
60
|
-
|
|
61
|
-
```sh
|
|
62
|
-
cp .env.example .env
|
|
63
|
-
# Edit .env and add your API key
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
**Method 3: Command Line Flag**
|
|
67
|
-
|
|
68
|
-
```sh
|
|
69
|
-
zai-cli --api-key your_api_key_here
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
**Method 4: User Settings File**
|
|
73
|
-
|
|
74
|
-
Create `~/.grok/user-settings.json`:
|
|
75
|
-
|
|
76
|
-
```json
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
```
|
|
72
|
+
|
|
73
|
+
**Method 1: Environment Variable**
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
export GROK_API_KEY=your_api_key_here
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Method 2: .env File**
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
cp .env.example .env
|
|
83
|
+
# Edit .env and add your API key
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Method 3: Command Line Flag**
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
zai-cli --api-key your_api_key_here
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Method 4: User Settings File**
|
|
93
|
+
|
|
94
|
+
Create `~/.grok/user-settings.json`:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"apiKey": "your_api_key_here"
|
|
99
|
+
}
|
|
100
|
+
```
|
|
81
101
|
|
|
82
102
|
3. (Optional, Recommended) Get your Morph API key from [Morph Dashboard](https://morphllm.com/dashboard/api-keys)
|
|
83
103
|
|
|
84
104
|
4. Set up your Morph API key for Fast Apply editing (choose one method):
|
|
85
105
|
|
|
86
|
-
**Method 1: Environment Variable**
|
|
87
|
-
|
|
88
|
-
```sh
|
|
89
|
-
export MORPH_API_KEY=your_morph_api_key_here
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
**Method 2: .env File**
|
|
93
|
-
|
|
94
|
-
```sh
|
|
95
|
-
# Add to your .env file
|
|
96
|
-
MORPH_API_KEY=your_morph_api_key_here
|
|
97
|
-
```
|
|
106
|
+
**Method 1: Environment Variable**
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
export MORPH_API_KEY=your_morph_api_key_here
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Method 2: .env File**
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
# Add to your .env file
|
|
116
|
+
MORPH_API_KEY=your_morph_api_key_here
|
|
117
|
+
```
|
|
98
118
|
|
|
99
119
|
### Custom Base URL (Optional)
|
|
100
120
|
|
|
@@ -61,7 +61,7 @@ export declare class GrokAgent extends EventEmitter {
|
|
|
61
61
|
private activeTaskAction;
|
|
62
62
|
private activeTaskColor;
|
|
63
63
|
private apiKeyEnvVar;
|
|
64
|
-
private
|
|
64
|
+
private pendingContextEditSession;
|
|
65
65
|
constructor(apiKey: string, baseURL?: string, model?: string, maxToolRounds?: number, debugLogFile?: string, startupHookOutput?: string, temperature?: number, maxTokens?: number);
|
|
66
66
|
private startupHookOutput?;
|
|
67
67
|
private systemPrompt;
|
|
@@ -119,12 +119,12 @@ export declare class GrokAgent extends EventEmitter {
|
|
|
119
119
|
getActiveTask(): string;
|
|
120
120
|
getActiveTaskAction(): string;
|
|
121
121
|
getActiveTaskColor(): string;
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
setPendingContextEditSession(tmpJsonPath: string, contextFilePath: string): void;
|
|
123
|
+
getPendingContextEditSession(): {
|
|
124
124
|
tmpJsonPath: string;
|
|
125
125
|
contextFilePath: string;
|
|
126
126
|
} | null;
|
|
127
|
-
|
|
127
|
+
clearPendingContextEditSession(): void;
|
|
128
128
|
setPersona(persona: string, color?: string): Promise<{
|
|
129
129
|
success: boolean;
|
|
130
130
|
error?: string;
|
package/dist/agent/grok-agent.js
CHANGED
|
@@ -9,6 +9,67 @@ import { EventEmitter } from "events";
|
|
|
9
9
|
import { createTokenCounter } from "../utils/token-counter.js";
|
|
10
10
|
import { getSettingsManager } from "../utils/settings-manager.js";
|
|
11
11
|
import { executeOperationHook, executeToolApprovalHook, applyHookCommands } from "../utils/hook-executor.js";
|
|
12
|
+
// Interval (ms) between token count updates when streaming
|
|
13
|
+
const TOKEN_UPDATE_INTERVAL_MS = 250;
|
|
14
|
+
// Minimum delay (in ms) applied when stopping a task to ensure smooth UI/UX.
|
|
15
|
+
const MINIMUM_STOP_TASK_DELAY_MS = 3000;
|
|
16
|
+
// Maximum number of attempts to parse nested JSON strings in executeTool
|
|
17
|
+
const MAX_JSON_PARSE_ATTEMPTS = 5;
|
|
18
|
+
/**
|
|
19
|
+
* Threshold used to determine whether an AI response is "substantial" (in characters).
|
|
20
|
+
*/
|
|
21
|
+
const SUBSTANTIAL_RESPONSE_THRESHOLD = 50;
|
|
22
|
+
/**
|
|
23
|
+
* Extracts the first complete JSON object from a string.
|
|
24
|
+
* Handles duplicate/concatenated JSON objects (LLM bug) like: {"key":"val"}{"key":"val"}
|
|
25
|
+
* @param jsonString The string potentially containing concatenated JSON objects
|
|
26
|
+
* @returns The first complete JSON object, or the original string if no duplicates found
|
|
27
|
+
*/
|
|
28
|
+
function extractFirstJsonObject(jsonString) {
|
|
29
|
+
if (!jsonString.includes('}{'))
|
|
30
|
+
return jsonString;
|
|
31
|
+
try {
|
|
32
|
+
// Find the end of the first complete JSON object
|
|
33
|
+
let depth = 0;
|
|
34
|
+
let firstObjEnd = -1;
|
|
35
|
+
for (let i = 0; i < jsonString.length; i++) {
|
|
36
|
+
if (jsonString[i] === "{")
|
|
37
|
+
depth++;
|
|
38
|
+
if (jsonString[i] === "}") {
|
|
39
|
+
depth--;
|
|
40
|
+
if (depth === 0) {
|
|
41
|
+
firstObjEnd = i + 1;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (firstObjEnd > 0 && firstObjEnd < jsonString.length) {
|
|
47
|
+
// Extract and validate first object
|
|
48
|
+
const firstObj = jsonString.substring(0, firstObjEnd);
|
|
49
|
+
JSON.parse(firstObj); // Validate it's valid JSON
|
|
50
|
+
return firstObj;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// If extraction fails, return the original string
|
|
55
|
+
}
|
|
56
|
+
return jsonString;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Cleans up LLM-generated JSON argument strings for tool calls.
|
|
60
|
+
* Removes duplicate/concatenated JSON objects and trims.
|
|
61
|
+
* @param args The raw arguments string from the tool call
|
|
62
|
+
* @returns Cleaned and sanitized argument string
|
|
63
|
+
*/
|
|
64
|
+
function sanitizeToolArguments(args) {
|
|
65
|
+
let argsString = args?.trim() || "{}";
|
|
66
|
+
// Handle duplicate/concatenated JSON objects (LLM bug)
|
|
67
|
+
const extractedArgsString = extractFirstJsonObject(argsString);
|
|
68
|
+
if (extractedArgsString !== argsString) {
|
|
69
|
+
argsString = extractedArgsString;
|
|
70
|
+
}
|
|
71
|
+
return argsString;
|
|
72
|
+
}
|
|
12
73
|
export class GrokAgent extends EventEmitter {
|
|
13
74
|
grokClient;
|
|
14
75
|
textEditor;
|
|
@@ -44,7 +105,7 @@ export class GrokAgent extends EventEmitter {
|
|
|
44
105
|
activeTaskAction = "";
|
|
45
106
|
activeTaskColor = "white";
|
|
46
107
|
apiKeyEnvVar = "GROK_API_KEY";
|
|
47
|
-
|
|
108
|
+
pendingContextEditSession = null;
|
|
48
109
|
constructor(apiKey, baseURL, model, maxToolRounds, debugLogFile, startupHookOutput, temperature, maxTokens) {
|
|
49
110
|
super();
|
|
50
111
|
const manager = getSettingsManager();
|
|
@@ -350,33 +411,7 @@ Current working directory: ${process.cwd()}`;
|
|
|
350
411
|
// Clean up tool call arguments before adding to conversation history
|
|
351
412
|
// This prevents Ollama from rejecting malformed tool calls on subsequent API calls
|
|
352
413
|
const cleanedToolCalls = assistantMessage.tool_calls.map(toolCall => {
|
|
353
|
-
let argsString = toolCall.function.arguments
|
|
354
|
-
// Handle duplicate/concatenated JSON objects (LLM bug)
|
|
355
|
-
if (argsString.includes('}{')) {
|
|
356
|
-
try {
|
|
357
|
-
let depth = 0;
|
|
358
|
-
let firstObjEnd = -1;
|
|
359
|
-
for (let i = 0; i < argsString.length; i++) {
|
|
360
|
-
if (argsString[i] === '{')
|
|
361
|
-
depth++;
|
|
362
|
-
if (argsString[i] === '}') {
|
|
363
|
-
depth--;
|
|
364
|
-
if (depth === 0) {
|
|
365
|
-
firstObjEnd = i + 1;
|
|
366
|
-
break;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
if (firstObjEnd > 0 && firstObjEnd < argsString.length) {
|
|
371
|
-
const firstObj = argsString.substring(0, firstObjEnd);
|
|
372
|
-
JSON.parse(firstObj); // Validate
|
|
373
|
-
argsString = firstObj; // Use cleaned version
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
catch (e) {
|
|
377
|
-
// Keep original if cleaning fails
|
|
378
|
-
}
|
|
379
|
-
}
|
|
414
|
+
let argsString = sanitizeToolArguments(toolCall.function.arguments);
|
|
380
415
|
return {
|
|
381
416
|
...toolCall,
|
|
382
417
|
function: {
|
|
@@ -546,8 +581,8 @@ Current working directory: ${process.cwd()}`;
|
|
|
546
581
|
// - A structured response format that explicitly marks completion
|
|
547
582
|
// For now, we break immediately after a substantial response to avoid
|
|
548
583
|
// the cascade of duplicate responses caused by "give it one more chance" logic.
|
|
549
|
-
// If the AI provided a substantial response (>
|
|
550
|
-
if (assistantMessage.content && assistantMessage.content.trim().length >
|
|
584
|
+
// If the AI provided a substantial response (>SUBSTANTIAL_RESPONSE_THRESHOLD chars), task is complete
|
|
585
|
+
if (assistantMessage.content && assistantMessage.content.trim().length > SUBSTANTIAL_RESPONSE_THRESHOLD) {
|
|
551
586
|
break; // Task complete - bot gave a full response
|
|
552
587
|
}
|
|
553
588
|
// Short/empty response, give AI another chance
|
|
@@ -847,7 +882,7 @@ Current working directory: ${process.cwd()}`;
|
|
|
847
882
|
};
|
|
848
883
|
// Emit token count update
|
|
849
884
|
const now = Date.now();
|
|
850
|
-
if (now - lastTokenUpdate >
|
|
885
|
+
if (now - lastTokenUpdate > TOKEN_UPDATE_INTERVAL_MS) {
|
|
851
886
|
lastTokenUpdate = now;
|
|
852
887
|
yield {
|
|
853
888
|
type: "token_count",
|
|
@@ -875,33 +910,7 @@ Current working directory: ${process.cwd()}`;
|
|
|
875
910
|
// Clean up tool call arguments before adding to conversation history
|
|
876
911
|
// This prevents Ollama from rejecting malformed tool calls on subsequent API calls
|
|
877
912
|
const cleanedToolCalls = accumulatedMessage.tool_calls?.map(toolCall => {
|
|
878
|
-
let argsString = toolCall.function.arguments
|
|
879
|
-
// Handle duplicate/concatenated JSON objects (LLM bug)
|
|
880
|
-
if (argsString.includes('}{')) {
|
|
881
|
-
try {
|
|
882
|
-
let depth = 0;
|
|
883
|
-
let firstObjEnd = -1;
|
|
884
|
-
for (let i = 0; i < argsString.length; i++) {
|
|
885
|
-
if (argsString[i] === '{')
|
|
886
|
-
depth++;
|
|
887
|
-
if (argsString[i] === '}') {
|
|
888
|
-
depth--;
|
|
889
|
-
if (depth === 0) {
|
|
890
|
-
firstObjEnd = i + 1;
|
|
891
|
-
break;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
if (firstObjEnd > 0 && firstObjEnd < argsString.length) {
|
|
896
|
-
const firstObj = argsString.substring(0, firstObjEnd);
|
|
897
|
-
JSON.parse(firstObj); // Validate
|
|
898
|
-
argsString = firstObj; // Use cleaned version
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
catch (e) {
|
|
902
|
-
// Keep original if cleaning fails
|
|
903
|
-
}
|
|
904
|
-
}
|
|
913
|
+
let argsString = sanitizeToolArguments(toolCall.function.arguments);
|
|
905
914
|
return {
|
|
906
915
|
...toolCall,
|
|
907
916
|
function: {
|
|
@@ -1178,41 +1187,16 @@ Current working directory: ${process.cwd()}`;
|
|
|
1178
1187
|
// Handle duplicate/concatenated JSON objects (LLM bug)
|
|
1179
1188
|
// Pattern: {"key":"val"}{"key":"val"}
|
|
1180
1189
|
let hadDuplicateJson = false;
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
let firstObjEnd = -1;
|
|
1186
|
-
for (let i = 0; i < argsString.length; i++) {
|
|
1187
|
-
if (argsString[i] === '{')
|
|
1188
|
-
depth++;
|
|
1189
|
-
if (argsString[i] === '}') {
|
|
1190
|
-
depth--;
|
|
1191
|
-
if (depth === 0) {
|
|
1192
|
-
firstObjEnd = i + 1;
|
|
1193
|
-
break;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
if (firstObjEnd > 0 && firstObjEnd < argsString.length) {
|
|
1198
|
-
// Extract and validate first object
|
|
1199
|
-
const firstObj = argsString.substring(0, firstObjEnd);
|
|
1200
|
-
JSON.parse(firstObj); // Validate it's valid JSON
|
|
1201
|
-
// Use only the first object
|
|
1202
|
-
hadDuplicateJson = true;
|
|
1203
|
-
argsString = firstObj;
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
catch (e) {
|
|
1207
|
-
// If extraction fails, continue with original string
|
|
1208
|
-
// The error will be caught by the main JSON.parse below
|
|
1209
|
-
}
|
|
1190
|
+
const extractedArgsString = extractFirstJsonObject(argsString);
|
|
1191
|
+
if (extractedArgsString !== argsString) {
|
|
1192
|
+
hadDuplicateJson = true;
|
|
1193
|
+
argsString = extractedArgsString;
|
|
1210
1194
|
}
|
|
1211
1195
|
let args = JSON.parse(argsString);
|
|
1212
1196
|
// Handle multiple layers of JSON encoding (API bug)
|
|
1213
1197
|
// Keep parsing until we get an object, not a string
|
|
1214
1198
|
let parseCount = 0;
|
|
1215
|
-
while (typeof args === 'string' && parseCount <
|
|
1199
|
+
while (typeof args === 'string' && parseCount < MAX_JSON_PARSE_ATTEMPTS) {
|
|
1216
1200
|
parseCount++;
|
|
1217
1201
|
try {
|
|
1218
1202
|
args = JSON.parse(args);
|
|
@@ -1561,14 +1545,14 @@ Current working directory: ${process.cwd()}`;
|
|
|
1561
1545
|
getActiveTaskColor() {
|
|
1562
1546
|
return this.activeTaskColor;
|
|
1563
1547
|
}
|
|
1564
|
-
|
|
1565
|
-
this.
|
|
1548
|
+
setPendingContextEditSession(tmpJsonPath, contextFilePath) {
|
|
1549
|
+
this.pendingContextEditSession = { tmpJsonPath, contextFilePath };
|
|
1566
1550
|
}
|
|
1567
|
-
|
|
1568
|
-
return this.
|
|
1551
|
+
getPendingContextEditSession() {
|
|
1552
|
+
return this.pendingContextEditSession;
|
|
1569
1553
|
}
|
|
1570
|
-
|
|
1571
|
-
this.
|
|
1554
|
+
clearPendingContextEditSession() {
|
|
1555
|
+
this.pendingContextEditSession = null;
|
|
1572
1556
|
}
|
|
1573
1557
|
async setPersona(persona, color) {
|
|
1574
1558
|
// Execute hook if configured
|
|
@@ -1908,7 +1892,7 @@ Current working directory: ${process.cwd()}`;
|
|
|
1908
1892
|
}
|
|
1909
1893
|
// Calculate remaining time to meet 3-second minimum
|
|
1910
1894
|
const elapsed = Date.now() - startTime;
|
|
1911
|
-
const minimumDelay =
|
|
1895
|
+
const minimumDelay = MINIMUM_STOP_TASK_DELAY_MS;
|
|
1912
1896
|
const remainingDelay = Math.max(0, minimumDelay - elapsed);
|
|
1913
1897
|
// Wait for remaining time if needed
|
|
1914
1898
|
if (remainingDelay > 0) {
|
|
@@ -2410,7 +2394,7 @@ Current working directory: ${process.cwd()}`;
|
|
|
2410
2394
|
this.emit('modelChange', { model });
|
|
2411
2395
|
}
|
|
2412
2396
|
else {
|
|
2413
|
-
console.warn(
|
|
2397
|
+
console.warn("Failed to restore backend: API key not found in environment.");
|
|
2414
2398
|
}
|
|
2415
2399
|
}
|
|
2416
2400
|
catch (error) {
|