llm-tool-parser 0.0.1
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 +187 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +17 -0
- package/lib/parser.d.ts +26 -0
- package/lib/parser.js +565 -0
- package/lib/parser.test.d.ts +1 -0
- package/lib/parser.test.js +351 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# llm-tool-parser
|
|
2
|
+
|
|
3
|
+
A TypeScript utility for extracting and normalizing tool calls from large language model outputs.
|
|
4
|
+
|
|
5
|
+
It supports both native tool calling formats (OpenAI `tool_calls`, Claude `tool_use`, etc.) and “text-based tool calls” commonly produced by open-source or fine-tuned models.
|
|
6
|
+
|
|
7
|
+
The library converts inconsistent, streaming, or partially corrupted LLM outputs into a clean and unified `ToolCall` structure, making it easier to build reliable agent runtimes.
|
|
8
|
+
|
|
9
|
+
### Key features
|
|
10
|
+
|
|
11
|
+
* Supports multiple LLM ecosystems (OpenAI, Claude, DeepSeek, Qwen, etc.)
|
|
12
|
+
* Extracts tool calls from raw text, JSON, XML, and hybrid formats
|
|
13
|
+
* Handles streaming, trailing text, and malformed outputs
|
|
14
|
+
* Normalizes all inputs into a unified `ToolCall` schema
|
|
15
|
+
* Works with both structured tool calling and prompt-based agents
|
|
16
|
+
|
|
17
|
+
## Simple usage
|
|
18
|
+
|
|
19
|
+
Examples below are based on `src/parser.test.ts`.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { parseLlmToolCalls } from 'llm-tool-parser'
|
|
23
|
+
|
|
24
|
+
const text =
|
|
25
|
+
'{"tool_calls":[{"tool_name":"fileRead","arguments":{"filePath":"/tmp/a.ts"}}]}'
|
|
26
|
+
|
|
27
|
+
const { content, toolCalls } = parseLlmToolCalls(text)
|
|
28
|
+
|
|
29
|
+
console.log(content)
|
|
30
|
+
console.log(toolCalls[0]?.name)
|
|
31
|
+
console.log(toolCalls[0]?.arguments)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
You can also parse model outputs that contain extra text:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const text =
|
|
38
|
+
'让我来查看一下\n{"tool_calls":[{"tool_name":"fileRead","arguments":{"filePath":"/a.ts"}}]}'
|
|
39
|
+
|
|
40
|
+
const { content, toolCalls } = parseLlmToolCalls(text)
|
|
41
|
+
|
|
42
|
+
console.log(content) // 让我来查看一下
|
|
43
|
+
console.log(toolCalls)
|
|
44
|
+
/**
|
|
45
|
+
[
|
|
46
|
+
{
|
|
47
|
+
id: 'call_1780567643675_6a8b',
|
|
48
|
+
name: 'fileRead',
|
|
49
|
+
arguments: { filePath: '/a.ts' }
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
*/
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
And code-fenced JSON is supported too:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const text =
|
|
59
|
+
'我来读取文件\n```json\n{"tool_calls":[{"tool_name":"fileRead","arguments":{"filePath":"/a.ts"}}]}\n```'
|
|
60
|
+
|
|
61
|
+
const { content, toolCalls } = parseLlmToolCalls(text)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Supported parsable formats
|
|
65
|
+
|
|
66
|
+
Based on all test cases in `src/parser.test.ts`, the parser currently supports these input patterns:
|
|
67
|
+
|
|
68
|
+
### 1. OpenAI-style `tool_calls`
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{"tool_calls":[{"tool_name":"fileRead","arguments":{"filePath":"/tmp/a.ts"}}]}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- single or multiple calls in one `tool_calls` array
|
|
75
|
+
- multiple consecutive JSON blocks
|
|
76
|
+
- JSON at the start with trailing text
|
|
77
|
+
- plain text before the JSON block
|
|
78
|
+
- JSON inside fenced code blocks
|
|
79
|
+
|
|
80
|
+
### 2. Single `tool_name` object
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{"tool_name":"bash","arguments":{"command":"ls"}}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 3. `[tool_calls]` marker + JSON objects
|
|
87
|
+
|
|
88
|
+
```txt
|
|
89
|
+
[tool_calls]{"tool":"grep","args":{"pattern":"hello"}}{"tool":"glob","args":{"pattern":"*.ts"}}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 4. `Action` / `Arguments` text format
|
|
93
|
+
|
|
94
|
+
```txt
|
|
95
|
+
Action: fileRead
|
|
96
|
+
Arguments: {"filePath":"/tmp/test.ts"}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Also supports non-JSON arguments:
|
|
100
|
+
|
|
101
|
+
```txt
|
|
102
|
+
Action: grep
|
|
103
|
+
Arguments: foo
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 5. XML-style tool tags
|
|
107
|
+
|
|
108
|
+
```xml
|
|
109
|
+
<fileRead>{"filePath":"/tmp/a.ts"}</fileRead>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- ignores non-tool tags like `<thinking>`
|
|
113
|
+
- supports XML inside fenced code blocks
|
|
114
|
+
|
|
115
|
+
### 6. CLI log style `[Called tools: ...]`
|
|
116
|
+
|
|
117
|
+
```txt
|
|
118
|
+
● [Called tools: fileRead({"filePath":"/tmp/a.ts"})]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Also supports multiple calls:
|
|
122
|
+
|
|
123
|
+
```txt
|
|
124
|
+
● [Called tools: grep({"pattern":"foo"}), glob({"pattern":"*.ts"})]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 7. OpenAI legacy `function_call`
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"function_call": {
|
|
132
|
+
"name": "fileRead",
|
|
133
|
+
"arguments": "{\"filePath\":\"/tmp/a.ts\"}"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 8. Claude `tool_use`
|
|
139
|
+
|
|
140
|
+
Content array form:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"content": [
|
|
145
|
+
{
|
|
146
|
+
"type": "tool_use",
|
|
147
|
+
"name": "fileRead",
|
|
148
|
+
"input": {"filePath": "/tmp/a.ts"}
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Standalone form:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"type": "tool_use",
|
|
159
|
+
"name": "fileRead",
|
|
160
|
+
"input": {"filePath": "/tmp/a.ts"}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 9. OpenAI Responses API `function_call`
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"type": "function_call",
|
|
169
|
+
"name": "fileRead",
|
|
170
|
+
"arguments": "{\"filePath\":\"/tmp/a.ts\"}"
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 10. Simple `tool` + `args` object
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{"tool":"fileRead","args":{"filePath":"/tmp/a.ts"}}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 11. Root array of tool calls
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
[
|
|
184
|
+
{"tool":"grep","args":{"pattern":"foo"}},
|
|
185
|
+
{"tool":"glob","args":{"pattern":"*.ts"}}
|
|
186
|
+
]
|
|
187
|
+
```
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './parser';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./parser"), exports);
|
package/lib/parser.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface LlmToolCall {
|
|
2
|
+
id?: string;
|
|
3
|
+
name: string;
|
|
4
|
+
arguments: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export interface LlmToolCallParseResult {
|
|
7
|
+
content: string;
|
|
8
|
+
toolCalls: LlmToolCall[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parse tool calls from LLM response text.
|
|
12
|
+
* Supports:
|
|
13
|
+
* 1. [tool_calls] marker format: [tool_calls]{"tool":"name","args":{...}}...
|
|
14
|
+
* 2. Standard JSON tool_calls embedded in text
|
|
15
|
+
* 3. Action/Arguments text format
|
|
16
|
+
* 4. XML tag format: <tool_name>{...}</tool_name>
|
|
17
|
+
*
|
|
18
|
+
* Returns the cleaned content (with tool call syntax removed) and parsed calls.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseLlmToolCalls(text: string): LlmToolCallParseResult;
|
|
21
|
+
/**
|
|
22
|
+
* Quick heuristic to detect if text is likely a tool call response.
|
|
23
|
+
* Used to suppress intermediate streaming of raw JSON that will be parsed
|
|
24
|
+
* as tool calls.
|
|
25
|
+
*/
|
|
26
|
+
export declare function looksLikeToolCall(text: string): boolean;
|
package/lib/parser.js
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseLlmToolCalls = parseLlmToolCalls;
|
|
4
|
+
exports.looksLikeToolCall = looksLikeToolCall;
|
|
5
|
+
const IGNORED_XML_TAGS = new Set([
|
|
6
|
+
"think", "thinking", "reflection", "output", "result", "answer", "response",
|
|
7
|
+
]);
|
|
8
|
+
/**
|
|
9
|
+
* Parse tool calls from LLM response text.
|
|
10
|
+
* Supports:
|
|
11
|
+
* 1. [tool_calls] marker format: [tool_calls]{"tool":"name","args":{...}}...
|
|
12
|
+
* 2. Standard JSON tool_calls embedded in text
|
|
13
|
+
* 3. Action/Arguments text format
|
|
14
|
+
* 4. XML tag format: <tool_name>{...}</tool_name>
|
|
15
|
+
*
|
|
16
|
+
* Returns the cleaned content (with tool call syntax removed) and parsed calls.
|
|
17
|
+
*/
|
|
18
|
+
function parseLlmToolCalls(text) {
|
|
19
|
+
if (!text)
|
|
20
|
+
return { content: "", toolCalls: [] };
|
|
21
|
+
const trimmedText = text.trim();
|
|
22
|
+
if (trimmedText.startsWith("[")) {
|
|
23
|
+
const parsed = tryParseLooseJson(trimmedText);
|
|
24
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
25
|
+
const toolCalls = extractToolCallsFromArray(parsed);
|
|
26
|
+
if (toolCalls.length > 0) {
|
|
27
|
+
return { content: "", toolCalls };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (trimmedText.startsWith("{")) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = tryParseLooseJson(trimmedText);
|
|
34
|
+
if (!parsed || typeof parsed !== "object") {
|
|
35
|
+
throw new Error("Invalid JSON object");
|
|
36
|
+
}
|
|
37
|
+
const result = tryParseJsonToolCalls(parsed);
|
|
38
|
+
if (result)
|
|
39
|
+
return result;
|
|
40
|
+
if (typeof parsed.tool_calls === "string") {
|
|
41
|
+
const nested = parseLlmToolCalls(parsed.tool_calls);
|
|
42
|
+
if (nested.toolCalls.length > 0)
|
|
43
|
+
return nested;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
const looseRoot = tryExtractToolCallsFromToolCallsArrayText(trimmedText);
|
|
48
|
+
if (looseRoot) {
|
|
49
|
+
const remainderResult = looseRoot.remainder
|
|
50
|
+
? parseLlmToolCalls(looseRoot.remainder)
|
|
51
|
+
: { content: "", toolCalls: [] };
|
|
52
|
+
return {
|
|
53
|
+
content: remainderResult.content,
|
|
54
|
+
toolCalls: [...looseRoot.toolCalls, ...remainderResult.toolCalls],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const allToolCalls = [];
|
|
58
|
+
let pos = 0;
|
|
59
|
+
let lastJsonEnd = 0;
|
|
60
|
+
while (pos < trimmedText.length) {
|
|
61
|
+
while (pos < trimmedText.length && trimmedText[pos] !== "{")
|
|
62
|
+
pos++;
|
|
63
|
+
if (pos >= trimmedText.length)
|
|
64
|
+
break;
|
|
65
|
+
const jsonStr = extractBalancedJson(trimmedText, pos);
|
|
66
|
+
if (!jsonStr)
|
|
67
|
+
break;
|
|
68
|
+
try {
|
|
69
|
+
const parsed = tryParseLooseJson(jsonStr);
|
|
70
|
+
if (!parsed || typeof parsed !== "object") {
|
|
71
|
+
throw new Error("Invalid JSON object");
|
|
72
|
+
}
|
|
73
|
+
const result = tryParseJsonToolCalls(parsed);
|
|
74
|
+
if (result && result.toolCalls.length > 0) {
|
|
75
|
+
allToolCalls.push(...result.toolCalls);
|
|
76
|
+
pos += jsonStr.length;
|
|
77
|
+
lastJsonEnd = pos;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Not valid JSON.
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
if (allToolCalls.length > 0) {
|
|
87
|
+
const content = trimmedText.substring(lastJsonEnd).trim();
|
|
88
|
+
return { content, toolCalls: allToolCalls };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const toolCallsJsonIdx = trimmedText.lastIndexOf('{"tool_calls"');
|
|
93
|
+
if (toolCallsJsonIdx >= 0) {
|
|
94
|
+
const jsonStr = extractBalancedJson(trimmedText, toolCallsJsonIdx);
|
|
95
|
+
if (jsonStr) {
|
|
96
|
+
const parsed = tryParseLooseJson(jsonStr);
|
|
97
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.tool_calls)) {
|
|
98
|
+
const toolCalls = parsed.tool_calls.map((tc) => ({
|
|
99
|
+
id: tc.id || generateId(),
|
|
100
|
+
name: tc.tool_name || tc.tool || tc.name || tc.function?.name,
|
|
101
|
+
arguments: parseToolArgs(tc.arguments || tc.args || tc.input || tc.function?.arguments),
|
|
102
|
+
}));
|
|
103
|
+
if (toolCalls.length > 0) {
|
|
104
|
+
const content = trimmedText.substring(0, toolCallsJsonIdx).trim();
|
|
105
|
+
return { content, toolCalls };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const toolCallsMarkerIdx = text.indexOf("[tool_calls]");
|
|
111
|
+
if (toolCallsMarkerIdx !== -1) {
|
|
112
|
+
const content = text.substring(0, toolCallsMarkerIdx).trim();
|
|
113
|
+
const toolCallsStr = text.substring(toolCallsMarkerIdx + "[tool_calls]".length).trim();
|
|
114
|
+
const toolCalls = parseConsecutiveJsonObjects(toolCallsStr);
|
|
115
|
+
if (toolCalls.length > 0) {
|
|
116
|
+
return { content, toolCalls };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const jsonToolCallsMatch = text.match(/```json\s*\n?\s*\{[\s\S]*?"tool_calls"\s*:\s*\[[\s\S]*?\]\s*\}?\s*\n?\s*```/);
|
|
120
|
+
if (jsonToolCallsMatch) {
|
|
121
|
+
const jsonStr = jsonToolCallsMatch[0]
|
|
122
|
+
.replace(/```json\s*\n?/, "")
|
|
123
|
+
.replace(/\n?\s*```$/, "")
|
|
124
|
+
.trim();
|
|
125
|
+
const parsed = tryParseLooseJson(jsonStr);
|
|
126
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.tool_calls)) {
|
|
127
|
+
const toolCalls = parsed.tool_calls.map((tc) => ({
|
|
128
|
+
id: tc.id,
|
|
129
|
+
name: tc.function?.name || tc.name || tc.tool,
|
|
130
|
+
arguments: parseToolArgs(tc.function?.arguments || tc.arguments || tc.args),
|
|
131
|
+
}));
|
|
132
|
+
const content = text.replace(jsonToolCallsMatch[0], "").trim();
|
|
133
|
+
return { content, toolCalls };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const actionMatch = text.match(/Action:\s*(\w+)/);
|
|
137
|
+
if (actionMatch) {
|
|
138
|
+
let args = {};
|
|
139
|
+
const argsMatch = text.match(/Arguments:\s*([\s\S]*)/);
|
|
140
|
+
if (argsMatch) {
|
|
141
|
+
const argsBody = argsMatch[1].trim();
|
|
142
|
+
if (argsBody.startsWith("{")) {
|
|
143
|
+
const jsonStr = extractBalancedJson(argsBody, 0);
|
|
144
|
+
if (jsonStr) {
|
|
145
|
+
const parsedArgs = tryParseLooseJson(jsonStr);
|
|
146
|
+
if (parsedArgs && typeof parsedArgs === "object") {
|
|
147
|
+
args = parsedArgs;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
args = { value: argsBody };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
args = { value: argsBody };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (argsBody) {
|
|
158
|
+
args = { value: argsBody };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const content = text
|
|
162
|
+
.replace(/Action:\s*\w+/, "")
|
|
163
|
+
.replace(/Arguments:\s*[\s\S]*/, "")
|
|
164
|
+
.trim();
|
|
165
|
+
return {
|
|
166
|
+
content,
|
|
167
|
+
toolCalls: [
|
|
168
|
+
{
|
|
169
|
+
id: generateId(),
|
|
170
|
+
name: actionMatch[1],
|
|
171
|
+
arguments: args,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const xmlCodeBlockMatch = text.match(/```(?:xml|)\s*\n([\s\S]*?)\n\s*```/);
|
|
177
|
+
if (xmlCodeBlockMatch) {
|
|
178
|
+
const innerXml = xmlCodeBlockMatch[1];
|
|
179
|
+
const xmlInBlockPattern = /<(\w+)>\s*([\s\S]*?)\s*<\/\1>/g;
|
|
180
|
+
const xmlInBlockResults = [];
|
|
181
|
+
let xmlInBlockMatch;
|
|
182
|
+
while ((xmlInBlockMatch = xmlInBlockPattern.exec(innerXml)) !== null) {
|
|
183
|
+
const tagName = xmlInBlockMatch[1];
|
|
184
|
+
if (IGNORED_XML_TAGS.has(tagName.toLowerCase()))
|
|
185
|
+
continue;
|
|
186
|
+
const innerContent = xmlInBlockMatch[2].trim();
|
|
187
|
+
const args = tryParseLooseJson(innerContent);
|
|
188
|
+
if (args && typeof args === "object") {
|
|
189
|
+
xmlInBlockResults.push({
|
|
190
|
+
id: generateId(),
|
|
191
|
+
name: tagName,
|
|
192
|
+
arguments: args,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (xmlInBlockResults.length > 0) {
|
|
197
|
+
const content = text.replace(xmlCodeBlockMatch[0], "").trim();
|
|
198
|
+
return { content, toolCalls: xmlInBlockResults };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const xmlPattern = /<(\w+)>\s*(\{[\s\S]*?\})\s*<\/\1>/g;
|
|
202
|
+
const xmlResults = [];
|
|
203
|
+
let xmlMatch;
|
|
204
|
+
let cleanText = text;
|
|
205
|
+
while ((xmlMatch = xmlPattern.exec(text)) !== null) {
|
|
206
|
+
const tagName = xmlMatch[1];
|
|
207
|
+
if (IGNORED_XML_TAGS.has(tagName.toLowerCase())) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const innerContent = xmlMatch[2].trim();
|
|
211
|
+
const args = tryParseLooseJson(innerContent);
|
|
212
|
+
if (args && typeof args === "object") {
|
|
213
|
+
xmlResults.push({
|
|
214
|
+
id: generateId(),
|
|
215
|
+
name: tagName,
|
|
216
|
+
arguments: args,
|
|
217
|
+
});
|
|
218
|
+
cleanText = cleanText.replace(xmlMatch[0], "");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (xmlResults.length > 0) {
|
|
222
|
+
return { content: cleanText.trim(), toolCalls: xmlResults };
|
|
223
|
+
}
|
|
224
|
+
const calledToolsIdx = text.search(/\[Called tools?:\s*/);
|
|
225
|
+
if (calledToolsIdx !== -1) {
|
|
226
|
+
const markerMatch = text.substring(calledToolsIdx).match(/^\[Called tools?:\s*/);
|
|
227
|
+
if (markerMatch) {
|
|
228
|
+
const innerStart = calledToolsIdx + markerMatch[0].length;
|
|
229
|
+
const calledToolCalls = parseCalledToolsFormat(text.substring(innerStart));
|
|
230
|
+
if (calledToolCalls.length > 0) {
|
|
231
|
+
const content = text.substring(0, calledToolsIdx).replace(/●\s*$/, "").trim();
|
|
232
|
+
return { content, toolCalls: calledToolCalls };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { content: text, toolCalls: [] };
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Quick heuristic to detect if text is likely a tool call response.
|
|
240
|
+
* Used to suppress intermediate streaming of raw JSON that will be parsed
|
|
241
|
+
* as tool calls.
|
|
242
|
+
*/
|
|
243
|
+
function looksLikeToolCall(text) {
|
|
244
|
+
const t = text.trimStart();
|
|
245
|
+
if (t.startsWith("{") || t.startsWith("[tool_calls]") || t.startsWith("[{"))
|
|
246
|
+
return true;
|
|
247
|
+
if (/^Action:\s*\w+/.test(t))
|
|
248
|
+
return true;
|
|
249
|
+
if (t.includes('{"tool_calls"'))
|
|
250
|
+
return true;
|
|
251
|
+
if (t.includes('"function_call"'))
|
|
252
|
+
return true;
|
|
253
|
+
if (t.includes('"tool_use"'))
|
|
254
|
+
return true;
|
|
255
|
+
if (/\[Called tools?:/.test(t))
|
|
256
|
+
return true;
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
function generateId() {
|
|
260
|
+
return `call_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
261
|
+
}
|
|
262
|
+
function tryParseJsonToolCalls(parsed) {
|
|
263
|
+
if (Array.isArray(parsed.tool_calls) && parsed.tool_calls.length > 0) {
|
|
264
|
+
const toolCalls = parsed.tool_calls.map((tc) => ({
|
|
265
|
+
id: tc.id || generateId(),
|
|
266
|
+
name: tc.tool_name || tc.tool || tc.name || tc.function?.name,
|
|
267
|
+
arguments: parseToolArgs(tc.arguments || tc.args || tc.input || tc.function?.arguments),
|
|
268
|
+
}));
|
|
269
|
+
return { content: "", toolCalls };
|
|
270
|
+
}
|
|
271
|
+
if (parsed.function_call?.name) {
|
|
272
|
+
return {
|
|
273
|
+
content: "",
|
|
274
|
+
toolCalls: [{
|
|
275
|
+
id: generateId(),
|
|
276
|
+
name: parsed.function_call.name,
|
|
277
|
+
arguments: parseToolArgs(parsed.function_call.arguments),
|
|
278
|
+
}],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (Array.isArray(parsed.content)) {
|
|
282
|
+
const toolUseBlocks = parsed.content.filter((b) => b.type === "tool_use" && b.name);
|
|
283
|
+
if (toolUseBlocks.length > 0) {
|
|
284
|
+
const toolCalls = toolUseBlocks.map((b) => ({
|
|
285
|
+
id: b.id || generateId(),
|
|
286
|
+
name: b.name,
|
|
287
|
+
arguments: parseToolArgs(b.input || b.arguments || b.args),
|
|
288
|
+
}));
|
|
289
|
+
return { content: "", toolCalls };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (parsed.type === "tool_use" && parsed.name) {
|
|
293
|
+
return {
|
|
294
|
+
content: "",
|
|
295
|
+
toolCalls: [{
|
|
296
|
+
id: parsed.id || generateId(),
|
|
297
|
+
name: parsed.name,
|
|
298
|
+
arguments: parseToolArgs(parsed.input || parsed.arguments || parsed.args),
|
|
299
|
+
}],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (parsed.type === "function_call" && parsed.name) {
|
|
303
|
+
return {
|
|
304
|
+
content: "",
|
|
305
|
+
toolCalls: [{
|
|
306
|
+
id: parsed.call_id || generateId(),
|
|
307
|
+
name: parsed.name,
|
|
308
|
+
arguments: parseToolArgs(parsed.arguments || parsed.args),
|
|
309
|
+
}],
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const name = parsed.tool_name || parsed.tool || parsed.function?.name || parsed.name;
|
|
313
|
+
if (name && typeof name === "string") {
|
|
314
|
+
const args = parseToolArgs(parsed.arguments || parsed.args || parsed.input || parsed.function?.arguments);
|
|
315
|
+
return {
|
|
316
|
+
content: "",
|
|
317
|
+
toolCalls: [{ id: generateId(), name, arguments: args }],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
function extractToolCallsFromArray(arr) {
|
|
323
|
+
const results = [];
|
|
324
|
+
for (const item of arr) {
|
|
325
|
+
if (typeof item !== "object" || item === null)
|
|
326
|
+
continue;
|
|
327
|
+
const name = item.tool_name || item.tool || item.name || item.function?.name;
|
|
328
|
+
if (!name || typeof name !== "string")
|
|
329
|
+
continue;
|
|
330
|
+
results.push({
|
|
331
|
+
id: item.id || item.call_id || generateId(),
|
|
332
|
+
name,
|
|
333
|
+
arguments: parseToolArgs(item.arguments || item.args || item.input || item.function?.arguments),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return results;
|
|
337
|
+
}
|
|
338
|
+
function parseConsecutiveJsonObjects(text) {
|
|
339
|
+
const results = [];
|
|
340
|
+
let pos = 0;
|
|
341
|
+
while (pos < text.length) {
|
|
342
|
+
while (pos < text.length && /\s/.test(text[pos]))
|
|
343
|
+
pos++;
|
|
344
|
+
if (pos >= text.length)
|
|
345
|
+
break;
|
|
346
|
+
if (text[pos] !== "{")
|
|
347
|
+
break;
|
|
348
|
+
const jsonStr = extractBalancedJson(text, pos);
|
|
349
|
+
if (!jsonStr)
|
|
350
|
+
break;
|
|
351
|
+
const obj = tryParseLooseJson(jsonStr);
|
|
352
|
+
if (!obj || typeof obj !== "object") {
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
const toolName = obj.tool || obj.name || obj.function?.name;
|
|
356
|
+
const toolArgs = obj.args || obj.arguments || obj.function?.arguments || {};
|
|
357
|
+
if (toolName) {
|
|
358
|
+
results.push({
|
|
359
|
+
id: generateId(),
|
|
360
|
+
name: toolName,
|
|
361
|
+
arguments: parseToolArgs(toolArgs),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
pos += jsonStr.length;
|
|
365
|
+
}
|
|
366
|
+
return results;
|
|
367
|
+
}
|
|
368
|
+
function parseCalledToolsFormat(inner) {
|
|
369
|
+
const results = [];
|
|
370
|
+
let pos = 0;
|
|
371
|
+
while (pos < inner.length) {
|
|
372
|
+
while (pos < inner.length && /[\s,\]]/.test(inner[pos]))
|
|
373
|
+
pos++;
|
|
374
|
+
if (pos >= inner.length)
|
|
375
|
+
break;
|
|
376
|
+
if (!/[a-zA-Z_]/.test(inner[pos]))
|
|
377
|
+
break;
|
|
378
|
+
const nameStart = pos;
|
|
379
|
+
while (pos < inner.length && /\w/.test(inner[pos]))
|
|
380
|
+
pos++;
|
|
381
|
+
const toolName = inner.substring(nameStart, pos);
|
|
382
|
+
while (pos < inner.length && inner[pos] === " ")
|
|
383
|
+
pos++;
|
|
384
|
+
if (pos >= inner.length || inner[pos] !== "(")
|
|
385
|
+
break;
|
|
386
|
+
pos++;
|
|
387
|
+
const argsStart = pos;
|
|
388
|
+
let args = {};
|
|
389
|
+
let parsed = false;
|
|
390
|
+
if (pos < inner.length && inner[pos] === "{") {
|
|
391
|
+
const jsonStr = extractBalancedJson(inner, pos);
|
|
392
|
+
if (jsonStr) {
|
|
393
|
+
pos += jsonStr.length;
|
|
394
|
+
const parsedArgs = tryParseLooseJson(jsonStr);
|
|
395
|
+
if (parsedArgs && typeof parsedArgs === "object") {
|
|
396
|
+
args = parsedArgs;
|
|
397
|
+
parsed = true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!parsed) {
|
|
402
|
+
while (pos < inner.length && inner[pos] !== ")")
|
|
403
|
+
pos++;
|
|
404
|
+
const rawArgs = inner.substring(argsStart, pos).trim();
|
|
405
|
+
if (rawArgs) {
|
|
406
|
+
const parsedArgs = tryParseLooseJson(rawArgs);
|
|
407
|
+
if (parsedArgs && typeof parsedArgs === "object") {
|
|
408
|
+
args = parsedArgs;
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
args = {};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
while (pos < inner.length && inner[pos] !== ")")
|
|
416
|
+
pos++;
|
|
417
|
+
if (pos < inner.length)
|
|
418
|
+
pos++;
|
|
419
|
+
results.push({
|
|
420
|
+
id: generateId(),
|
|
421
|
+
name: toolName,
|
|
422
|
+
arguments: args,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return results;
|
|
426
|
+
}
|
|
427
|
+
function parseToolArgs(args) {
|
|
428
|
+
if (!args)
|
|
429
|
+
return {};
|
|
430
|
+
if (typeof args === "string") {
|
|
431
|
+
return tryParseLooseJson(args) ?? {};
|
|
432
|
+
}
|
|
433
|
+
if (typeof args === "object")
|
|
434
|
+
return args;
|
|
435
|
+
return {};
|
|
436
|
+
}
|
|
437
|
+
function tryExtractToolCallsFromToolCallsArrayText(text) {
|
|
438
|
+
const markerMatch = /"tool_calls"\s*:\s*\[/.exec(text);
|
|
439
|
+
if (!markerMatch)
|
|
440
|
+
return null;
|
|
441
|
+
const toolCalls = [];
|
|
442
|
+
let pos = markerMatch.index + markerMatch[0].length;
|
|
443
|
+
while (pos < text.length) {
|
|
444
|
+
while (pos < text.length && /\s/.test(text[pos]))
|
|
445
|
+
pos++;
|
|
446
|
+
if (pos >= text.length)
|
|
447
|
+
break;
|
|
448
|
+
const ch = text[pos];
|
|
449
|
+
if (ch === "]") {
|
|
450
|
+
pos++;
|
|
451
|
+
while (pos < text.length && /[\s}]/.test(text[pos]))
|
|
452
|
+
pos++;
|
|
453
|
+
return {
|
|
454
|
+
toolCalls,
|
|
455
|
+
remainder: text.slice(pos).trim(),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
if (ch === "," || ch === "}") {
|
|
459
|
+
pos++;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (ch !== "{") {
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
const jsonStr = extractBalancedJson(text, pos);
|
|
466
|
+
if (!jsonStr)
|
|
467
|
+
return null;
|
|
468
|
+
const parsed = tryParseLooseJson(jsonStr);
|
|
469
|
+
if (!parsed || typeof parsed !== "object")
|
|
470
|
+
return null;
|
|
471
|
+
const result = tryParseJsonToolCalls(parsed);
|
|
472
|
+
if (!result || result.toolCalls.length === 0)
|
|
473
|
+
return null;
|
|
474
|
+
toolCalls.push(...result.toolCalls);
|
|
475
|
+
pos += jsonStr.length;
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
function tryParseLooseJson(text) {
|
|
480
|
+
try {
|
|
481
|
+
return JSON.parse(text);
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
const sanitized = escapeLiteralControlCharsInJsonStrings(text);
|
|
485
|
+
if (sanitized === text) {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
return JSON.parse(sanitized);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function escapeLiteralControlCharsInJsonStrings(text) {
|
|
497
|
+
let result = "";
|
|
498
|
+
let inString = false;
|
|
499
|
+
let escape = false;
|
|
500
|
+
for (const ch of text) {
|
|
501
|
+
if (escape) {
|
|
502
|
+
result += ch;
|
|
503
|
+
escape = false;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (ch === "\\" && inString) {
|
|
507
|
+
result += ch;
|
|
508
|
+
escape = true;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (ch === "\"") {
|
|
512
|
+
result += ch;
|
|
513
|
+
inString = !inString;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (inString) {
|
|
517
|
+
if (ch === "\n") {
|
|
518
|
+
result += "\\n";
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (ch === "\r") {
|
|
522
|
+
result += "\\r";
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (ch === "\t") {
|
|
526
|
+
result += "\\t";
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
result += ch;
|
|
531
|
+
}
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
function extractBalancedJson(text, start) {
|
|
535
|
+
if (start < 0 || start >= text.length || text[start] !== "{")
|
|
536
|
+
return null;
|
|
537
|
+
let depth = 0;
|
|
538
|
+
let inString = false;
|
|
539
|
+
let escape = false;
|
|
540
|
+
for (let i = start; i < text.length; i++) {
|
|
541
|
+
const ch = text[i];
|
|
542
|
+
if (escape) {
|
|
543
|
+
escape = false;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (ch === "\\" && inString) {
|
|
547
|
+
escape = true;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (ch === "\"") {
|
|
551
|
+
inString = !inString;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (inString)
|
|
555
|
+
continue;
|
|
556
|
+
if (ch === "{")
|
|
557
|
+
depth++;
|
|
558
|
+
else if (ch === "}") {
|
|
559
|
+
depth--;
|
|
560
|
+
if (depth === 0)
|
|
561
|
+
return text.substring(start, i + 1);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const parser_1 = require("./parser");
|
|
5
|
+
(0, vitest_1.describe)('parseLlmToolCalls', () => {
|
|
6
|
+
(0, vitest_1.describe)('Format A: pure JSON with tool_calls array', () => {
|
|
7
|
+
(0, vitest_1.it)('parses a single tool call', () => {
|
|
8
|
+
const text = '{"tool_calls":[{"tool_name":"fileRead","arguments":{"filePath":"/tmp/a.ts"}}]}';
|
|
9
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
10
|
+
(0, vitest_1.expect)(content).toBe('');
|
|
11
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
12
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
13
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toEqual({ filePath: '/tmp/a.ts' });
|
|
14
|
+
});
|
|
15
|
+
(0, vitest_1.it)('parses multiple tool calls', () => {
|
|
16
|
+
const text = JSON.stringify({
|
|
17
|
+
tool_calls: [
|
|
18
|
+
{ tool_name: 'grep', arguments: { pattern: 'foo', path: '/src' } },
|
|
19
|
+
{ tool_name: 'glob', arguments: { pattern: '**/*.ts' } },
|
|
20
|
+
{ tool_name: 'listFiles', arguments: { path: '/src' } },
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
24
|
+
(0, vitest_1.expect)(content).toBe('');
|
|
25
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(3);
|
|
26
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('grep');
|
|
27
|
+
(0, vitest_1.expect)(toolCalls[1].name).toBe('glob');
|
|
28
|
+
(0, vitest_1.expect)(toolCalls[2].name).toBe('listFiles');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.describe)('Format B: single tool call without array wrapper', () => {
|
|
32
|
+
(0, vitest_1.it)('parses single tool_name object', () => {
|
|
33
|
+
const text = '{"tool_name":"bash","arguments":{"command":"ls"}}';
|
|
34
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
35
|
+
(0, vitest_1.expect)(content).toBe('');
|
|
36
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
37
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('bash');
|
|
38
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toEqual({ command: 'ls' });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
(0, vitest_1.describe)('Multiple consecutive tool_calls JSON blocks', () => {
|
|
42
|
+
(0, vitest_1.it)('parses two consecutive tool_calls blocks', () => {
|
|
43
|
+
const text = '{"tool_calls":[{"tool_name":"glob","arguments":{"pattern":"README*"}}]}{"tool_calls":[{"tool_name":"fileRead","arguments":{"filePath":"/tmp/a.ts"}}]}';
|
|
44
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
45
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(2);
|
|
46
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('glob');
|
|
47
|
+
(0, vitest_1.expect)(toolCalls[1].name).toBe('fileRead');
|
|
48
|
+
(0, vitest_1.expect)(content).toBe('');
|
|
49
|
+
});
|
|
50
|
+
(0, vitest_1.it)('parses three consecutive tool_calls blocks (real log scenario)', () => {
|
|
51
|
+
const text = '{"tool_calls":[{"tool_name":"glob","arguments":{"pattern":"README*","path":"/Users/someone/gitlab/mini-kode"}},{"tool_name":"fileRead","arguments":{"filePath":"/Users/someone/gitlab/mini-kode/package.json"}}]}{"tool_calls":[{"tool_name":"fileRead","arguments":{"filePath":"/Users/someone/gitlab/mini-kode/README.md"}}]}{"tool_calls":[{"tool_name":"fileEdit","arguments":{"filePath":"/Users/someone/gitlab/mini-kode/README.md","old_string":"# mini-kode\\n","new_string":"# mini-kode\\n\\n## Install\\n"}}]}';
|
|
52
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
53
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(4);
|
|
54
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('glob');
|
|
55
|
+
(0, vitest_1.expect)(toolCalls[1].name).toBe('fileRead');
|
|
56
|
+
(0, vitest_1.expect)(toolCalls[2].name).toBe('fileRead');
|
|
57
|
+
(0, vitest_1.expect)(toolCalls[3].name).toBe('fileEdit');
|
|
58
|
+
(0, vitest_1.expect)(content).toBe('');
|
|
59
|
+
});
|
|
60
|
+
(0, vitest_1.it)('parses consecutive tool_calls blocks with trailing text', () => {
|
|
61
|
+
const text = '{"tool_calls":[{"tool_name":"glob","arguments":{"pattern":"*.ts"}}]}{"tool_calls":[{"tool_name":"fileRead","arguments":{"filePath":"/a.ts"}}]}以上是文件内容';
|
|
62
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
63
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(2);
|
|
64
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('glob');
|
|
65
|
+
(0, vitest_1.expect)(toolCalls[1].name).toBe('fileRead');
|
|
66
|
+
(0, vitest_1.expect)(content).toBe('以上是文件内容');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.describe)('JSON at start with trailing text (the s.log bug)', () => {
|
|
70
|
+
(0, vitest_1.it)('parses tool_calls JSON followed by trailing text without separator', () => {
|
|
71
|
+
const text = '{"tool_calls":[{"tool_name":"grep","arguments":{"pattern":"\\\\.xxxx/skills","path":"/Users/someone/gitlab/xxx-xxxx-agent-cli"}},{"tool_name":"glob","arguments":{"pattern":"**/.xxxx/skills/**"}},{"tool_name":"listFiles","arguments":{"path":"/Users/someone/gitlab/xxx-xxxx-agent-cli/.xxxx"}}]}`.xxxx/skills` 没发现。\n当前仓库里 skill 相关主要是:`.agents/skills/`。';
|
|
72
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
73
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(3);
|
|
74
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('grep');
|
|
75
|
+
(0, vitest_1.expect)(toolCalls[1].name).toBe('glob');
|
|
76
|
+
(0, vitest_1.expect)(toolCalls[2].name).toBe('listFiles');
|
|
77
|
+
(0, vitest_1.expect)(content).toContain('.xxxx/skills');
|
|
78
|
+
});
|
|
79
|
+
(0, vitest_1.it)('parses tool_calls JSON followed by newline and text', () => {
|
|
80
|
+
const text = '{"tool_calls":[{"tool_name":"fileRead","arguments":{"filePath":"/tmp/x.ts"}}]}\n文件内容如下...';
|
|
81
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
82
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
83
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
84
|
+
(0, vitest_1.expect)(content).toBe('文件内容如下...');
|
|
85
|
+
});
|
|
86
|
+
(0, vitest_1.it)('parses tool_calls JSON string with newline', () => {
|
|
87
|
+
const text = `{"tool_calls":[{"tool_name":"fileEdit","arguments":{"filePath":"/Users/someone/gitlab/tyyf6hyazx6l0sgrbr/src/components/bottom-bar/index.tsx","old_string":" <View\n className={styles.btns}\n style={{\n paddingBottom: isIphoneX ? '40rpx' : '0rpx',\n }}\n >\n {showButtons.map(item => {\n","new_string":" <View\n className={styles.btns}\n style={{\n paddingBottom: isIphoneX\n ? 'calc(40rpx + env(safe-area-inset-bottom))'\n : 'env(safe-area-inset-bottom)',\n }}\n >\n {showButtons.map(item => {\n"}}},{"tool_name":"glob","arguments":{"pattern":"package.json","path":"/Users/someone/gitlab/tyyf6hyazx6l0sgrbr"}},{"tool_name":"glob","arguments":{"pattern":"pnpm-lock.yaml","path":"/Users/someone/gitlab/tyyf6hyazx6l0sgrbr"}}]}`;
|
|
88
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
89
|
+
(0, vitest_1.expect)(content).toBe('');
|
|
90
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(3);
|
|
91
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileEdit');
|
|
92
|
+
(0, vitest_1.expect)(toolCalls[1].name).toBe('glob');
|
|
93
|
+
(0, vitest_1.expect)(toolCalls[2].name).toBe('glob');
|
|
94
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toMatchObject({
|
|
95
|
+
filePath: '/Users/someone/gitlab/tyyf6hyazx6l0sgrbr/src/components/bottom-bar/index.tsx',
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
(0, vitest_1.describe)('Format 0.5: text followed by tool_calls JSON', () => {
|
|
100
|
+
(0, vitest_1.it)('parses text before JSON block', () => {
|
|
101
|
+
const text = '让我来查看一下\n{"tool_calls":[{"tool_name":"fileRead","arguments":{"filePath":"/a.ts"}}]}';
|
|
102
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
103
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
104
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
105
|
+
(0, vitest_1.expect)(content).toBe('让我来查看一下');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
(0, vitest_1.describe)('[tool_calls] marker format', () => {
|
|
109
|
+
(0, vitest_1.it)('parses [tool_calls] with consecutive JSON objects', () => {
|
|
110
|
+
const text = '我来搜索一下\n[tool_calls]{"tool":"grep","args":{"pattern":"hello"}}{"tool":"glob","args":{"pattern":"*.ts"}}';
|
|
111
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
112
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(2);
|
|
113
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('grep');
|
|
114
|
+
(0, vitest_1.expect)(toolCalls[1].name).toBe('glob');
|
|
115
|
+
(0, vitest_1.expect)(content).toBe('我来搜索一下');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
(0, vitest_1.describe)('Action/Arguments format', () => {
|
|
119
|
+
(0, vitest_1.it)('parses Action and Arguments text format', () => {
|
|
120
|
+
const text = 'Action: fileRead\nArguments: {"filePath":"/tmp/test.ts"}';
|
|
121
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
122
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
123
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
124
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toEqual({ filePath: '/tmp/test.ts' });
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
(0, vitest_1.describe)('XML tag format', () => {
|
|
128
|
+
(0, vitest_1.it)('parses XML-style tool calls', () => {
|
|
129
|
+
const text = '<fileRead>{"filePath":"/tmp/a.ts"}</fileRead>';
|
|
130
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
131
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
132
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
133
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toEqual({ filePath: '/tmp/a.ts' });
|
|
134
|
+
});
|
|
135
|
+
(0, vitest_1.it)('ignores thinking/reflection tags', () => {
|
|
136
|
+
const text = '<thinking>{"internal":"reasoning"}</thinking>\n<fileRead>{"filePath":"/a.ts"}</fileRead>';
|
|
137
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
138
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
139
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
(0, vitest_1.describe)('[Called tools: ...] format', () => {
|
|
143
|
+
(0, vitest_1.it)('parses single called tool', () => {
|
|
144
|
+
const text = '● [Called tools: fileRead({"filePath":"/tmp/a.ts"})]';
|
|
145
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
146
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
147
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
148
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toEqual({ filePath: '/tmp/a.ts' });
|
|
149
|
+
});
|
|
150
|
+
(0, vitest_1.it)('parses multiple called tools', () => {
|
|
151
|
+
const text = '● [Called tools: grep({"pattern":"foo"}), glob({"pattern":"*.ts"})]';
|
|
152
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
153
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(2);
|
|
154
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('grep');
|
|
155
|
+
(0, vitest_1.expect)(toolCalls[1].name).toBe('glob');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
(0, vitest_1.describe)('function_call format (GPT3.5/4 legacy)', () => {
|
|
159
|
+
(0, vitest_1.it)('parses function_call format', () => {
|
|
160
|
+
const text = JSON.stringify({
|
|
161
|
+
function_call: {
|
|
162
|
+
name: 'fileRead',
|
|
163
|
+
arguments: JSON.stringify({ filePath: '/tmp/a.ts' }),
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
const { toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
167
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
168
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
169
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toEqual({ filePath: '/tmp/a.ts' });
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
(0, vitest_1.describe)('Claude tool_use content block', () => {
|
|
173
|
+
(0, vitest_1.it)('parses claude tool_use block', () => {
|
|
174
|
+
const text = JSON.stringify({
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: 'tool_use',
|
|
178
|
+
id: 'toolu_123',
|
|
179
|
+
name: 'fileRead',
|
|
180
|
+
input: { filePath: '/tmp/a.ts' },
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
const { toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
185
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
186
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
187
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toEqual({ filePath: '/tmp/a.ts' });
|
|
188
|
+
});
|
|
189
|
+
(0, vitest_1.it)('parses multiple tool_use blocks in content array', () => {
|
|
190
|
+
const text = JSON.stringify({
|
|
191
|
+
content: [
|
|
192
|
+
{
|
|
193
|
+
type: 'tool_use',
|
|
194
|
+
id: 'toolu_1',
|
|
195
|
+
name: 'grep',
|
|
196
|
+
input: { pattern: 'foo' },
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: 'tool_use',
|
|
200
|
+
id: 'toolu_2',
|
|
201
|
+
name: 'glob',
|
|
202
|
+
input: { pattern: '*.ts' },
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
const { toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
207
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(2);
|
|
208
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('grep');
|
|
209
|
+
(0, vitest_1.expect)(toolCalls[1].name).toBe('glob');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
(0, vitest_1.describe)('Claude standalone tool_use', () => {
|
|
213
|
+
(0, vitest_1.it)('parses standalone tool_use object', () => {
|
|
214
|
+
const text = JSON.stringify({
|
|
215
|
+
type: 'tool_use',
|
|
216
|
+
name: 'fileRead',
|
|
217
|
+
input: { filePath: '/tmp/a.ts' },
|
|
218
|
+
});
|
|
219
|
+
const { toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
220
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
221
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
222
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toEqual({ filePath: '/tmp/a.ts' });
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
(0, vitest_1.describe)('OpenAI Responses API format', () => {
|
|
226
|
+
(0, vitest_1.it)('parses responses api function_call', () => {
|
|
227
|
+
const text = JSON.stringify({
|
|
228
|
+
type: 'function_call',
|
|
229
|
+
call_id: 'call_123',
|
|
230
|
+
name: 'fileRead',
|
|
231
|
+
arguments: JSON.stringify({ filePath: '/tmp/a.ts' }),
|
|
232
|
+
});
|
|
233
|
+
const { toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
234
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
235
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
236
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toEqual({ filePath: '/tmp/a.ts' });
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
(0, vitest_1.describe)('tool + args single object format', () => {
|
|
240
|
+
(0, vitest_1.it)('parses tool + args format', () => {
|
|
241
|
+
const text = JSON.stringify({
|
|
242
|
+
tool: 'fileRead',
|
|
243
|
+
args: { filePath: '/tmp/a.ts' },
|
|
244
|
+
});
|
|
245
|
+
const { toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
246
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
247
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
248
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toEqual({ filePath: '/tmp/a.ts' });
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
(0, vitest_1.describe)('root array format', () => {
|
|
252
|
+
(0, vitest_1.it)('parses root array tool calls', () => {
|
|
253
|
+
const text = JSON.stringify([
|
|
254
|
+
{ tool: 'grep', args: { pattern: 'foo' } },
|
|
255
|
+
{ tool: 'glob', args: { pattern: '*.ts' } },
|
|
256
|
+
]);
|
|
257
|
+
const { toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
258
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(2);
|
|
259
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('grep');
|
|
260
|
+
(0, vitest_1.expect)(toolCalls[1].name).toBe('glob');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
(0, vitest_1.describe)('XML inside code block', () => {
|
|
264
|
+
(0, vitest_1.it)('parses xml inside code block', () => {
|
|
265
|
+
const text = `
|
|
266
|
+
\`\`\`xml
|
|
267
|
+
<fileRead>{"filePath":"/tmp/a.ts"}</fileRead>
|
|
268
|
+
\`\`\`
|
|
269
|
+
`;
|
|
270
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
271
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
272
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
273
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toEqual({ filePath: '/tmp/a.ts' });
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
(0, vitest_1.describe)('non-JSON arguments', () => {
|
|
277
|
+
(0, vitest_1.it)('handles non-json arguments as value string', () => {
|
|
278
|
+
const text = `
|
|
279
|
+
Action: grep
|
|
280
|
+
Arguments: foo
|
|
281
|
+
`;
|
|
282
|
+
const { toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
283
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
284
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('grep');
|
|
285
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toEqual({ value: 'foo' });
|
|
286
|
+
});
|
|
287
|
+
(0, vitest_1.it)('handles multiline non-json arguments', () => {
|
|
288
|
+
const text = `Action: grep
|
|
289
|
+
Arguments:
|
|
290
|
+
foo`;
|
|
291
|
+
const { toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
292
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
293
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('grep');
|
|
294
|
+
(0, vitest_1.expect)(toolCalls[0].arguments).toEqual({ value: 'foo' });
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
(0, vitest_1.describe)('plain text (no tool calls)', () => {
|
|
298
|
+
(0, vitest_1.it)('returns text as content with empty toolCalls', () => {
|
|
299
|
+
const text = '这是一段普通的回复文本,没有任何工具调用。';
|
|
300
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
301
|
+
(0, vitest_1.expect)(content).toBe(text);
|
|
302
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(0);
|
|
303
|
+
});
|
|
304
|
+
(0, vitest_1.it)('handles empty string', () => {
|
|
305
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)('');
|
|
306
|
+
(0, vitest_1.expect)(content).toBe('');
|
|
307
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(0);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
(0, vitest_1.describe)('code block with tool_calls JSON', () => {
|
|
311
|
+
(0, vitest_1.it)('parses tool_calls inside ```json code fence', () => {
|
|
312
|
+
const text = '我来读取文件\n```json\n{"tool_calls":[{"tool_name":"fileRead","arguments":{"filePath":"/a.ts"}}]}\n```';
|
|
313
|
+
const { content, toolCalls } = (0, parser_1.parseLlmToolCalls)(text);
|
|
314
|
+
(0, vitest_1.expect)(toolCalls).toHaveLength(1);
|
|
315
|
+
(0, vitest_1.expect)(toolCalls[0].name).toBe('fileRead');
|
|
316
|
+
(0, vitest_1.expect)(content).toContain('我来读取文件');
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
(0, vitest_1.describe)('looksLikeToolCall', () => {
|
|
321
|
+
(0, vitest_1.it)('detects JSON starting with {', () => {
|
|
322
|
+
(0, vitest_1.expect)((0, parser_1.looksLikeToolCall)('{"tool_calls":[...]}')).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
(0, vitest_1.it)('detects [tool_calls] marker', () => {
|
|
325
|
+
(0, vitest_1.expect)((0, parser_1.looksLikeToolCall)('[tool_calls]{...}')).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
(0, vitest_1.it)('detects Action: format', () => {
|
|
328
|
+
(0, vitest_1.expect)((0, parser_1.looksLikeToolCall)('Action: fileRead\nArguments: {...}')).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
(0, vitest_1.it)('detects embedded tool_calls JSON', () => {
|
|
331
|
+
(0, vitest_1.expect)((0, parser_1.looksLikeToolCall)('一些文本 {"tool_calls":[...]}')).toBe(true);
|
|
332
|
+
});
|
|
333
|
+
(0, vitest_1.it)('detects [Called tools:] format', () => {
|
|
334
|
+
(0, vitest_1.expect)((0, parser_1.looksLikeToolCall)('● [Called tools: grep(...)]')).toBe(true);
|
|
335
|
+
});
|
|
336
|
+
(0, vitest_1.it)('detects root array format', () => {
|
|
337
|
+
(0, vitest_1.expect)((0, parser_1.looksLikeToolCall)('[{"tool":"grep","args":{}}]')).toBe(true);
|
|
338
|
+
});
|
|
339
|
+
(0, vitest_1.it)('detects function_call keyword', () => {
|
|
340
|
+
(0, vitest_1.expect)((0, parser_1.looksLikeToolCall)('{"function_call":{"name":"fileRead"}}')).toBe(true);
|
|
341
|
+
});
|
|
342
|
+
(0, vitest_1.it)('detects tool_use keyword', () => {
|
|
343
|
+
(0, vitest_1.expect)((0, parser_1.looksLikeToolCall)('{"type":"tool_use","name":"fileRead"}')).toBe(true);
|
|
344
|
+
});
|
|
345
|
+
(0, vitest_1.it)('returns false for plain text', () => {
|
|
346
|
+
(0, vitest_1.expect)((0, parser_1.looksLikeToolCall)('这是一段普通回复')).toBe(false);
|
|
347
|
+
});
|
|
348
|
+
(0, vitest_1.it)('returns false for markdown content', () => {
|
|
349
|
+
(0, vitest_1.expect)((0, parser_1.looksLikeToolCall)('## Heading\n\nSome content here')).toBe(false);
|
|
350
|
+
});
|
|
351
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "llm-tool-parser",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Universal LLM tool call extractor that normalizes messy outputs into a unified ToolCall format.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Saber2pr/llm-tool-parser"
|
|
8
|
+
},
|
|
9
|
+
"author": "saber2pr",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"files": [
|
|
12
|
+
"lib"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public",
|
|
16
|
+
"registry": "https://registry.npmjs.org/"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"llm",
|
|
20
|
+
"tool-calling",
|
|
21
|
+
"tool-calls",
|
|
22
|
+
"ai-tools",
|
|
23
|
+
"agent",
|
|
24
|
+
"ai-agent",
|
|
25
|
+
"llm-agent",
|
|
26
|
+
"tool-parser",
|
|
27
|
+
"tool-extraction",
|
|
28
|
+
"llm-output-parser"
|
|
29
|
+
],
|
|
30
|
+
"main": "./lib/index.js",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"start": "tsc --watch",
|
|
33
|
+
"build": "tsc",
|
|
34
|
+
"prepublishOnly": "yarn build",
|
|
35
|
+
"test": "vitest run src/*.test.ts",
|
|
36
|
+
"lint": "prettier --write ./src",
|
|
37
|
+
"prepare": "husky install"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.9.1",
|
|
42
|
+
"husky": "^7.0.2",
|
|
43
|
+
"lint-staged": "^11.1.2",
|
|
44
|
+
"prettier": "^2.4.1",
|
|
45
|
+
"typescript": "^5.9.2",
|
|
46
|
+
"vitest": "^4.1.8"
|
|
47
|
+
},
|
|
48
|
+
"lint-staged": {
|
|
49
|
+
"*.{js,jsx,ts,tsx}": [
|
|
50
|
+
"yarn lint",
|
|
51
|
+
"git add ."
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|