wormclaude 1.0.14 → 1.0.16
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/dist/agent.js +10 -1
- package/dist/api.js +12 -10
- package/dist/atmention.js +117 -0
- package/dist/auth.js +10 -0
- package/dist/cli.js +40 -6
- package/dist/commands.js +44 -8
- package/dist/errorsan.js +94 -0
- package/dist/markdown.js +221 -0
- package/dist/memory.js +41 -0
- package/dist/safejson.js +166 -0
- package/dist/streamparser.js +158 -0
- package/dist/subagents.js +119 -0
- package/dist/textclean.js +37 -0
- package/dist/theme.js +1 -1
- package/dist/tools.js +128 -14
- package/package.json +3 -2
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Terminal markdown render (ink). Gemini MarkdownDisplay + InlineMarkdownRenderer + TableRenderer'dan
|
|
2
|
+
// uyarlandı; WormClaude theme.ts'e bağlandı; lowlight YOK (kod blokları tek accent renk — görsel cila v1).
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Text, Box } from 'ink';
|
|
5
|
+
import stringWidth from 'string-width';
|
|
6
|
+
import { theme } from './theme.js';
|
|
7
|
+
// ── Inline markdown: **bold** *italik* ~~strike~~ `code` [text](url) <u>u</u> url ──
|
|
8
|
+
const INLINE_RE = /(\*\*.*?\*\*|\*.*?\*|_.*?_|~~.*?~~|\[.*?\]\(.*?\)|`+.+?`+|<u>.*?<\/u>|https?:\/\/\S+)/g;
|
|
9
|
+
export function RenderInline({ text }) {
|
|
10
|
+
if (!/[*_~`<[]|https?:/.test(text))
|
|
11
|
+
return React.createElement(Text, null, text);
|
|
12
|
+
const nodes = [];
|
|
13
|
+
let last = 0;
|
|
14
|
+
let m;
|
|
15
|
+
INLINE_RE.lastIndex = 0;
|
|
16
|
+
while ((m = INLINE_RE.exec(text)) !== null) {
|
|
17
|
+
if (m.index > last)
|
|
18
|
+
nodes.push(React.createElement(Text, { key: `t${last}` }, text.slice(last, m.index)));
|
|
19
|
+
const f = m[0];
|
|
20
|
+
const key = `m${m.index}`;
|
|
21
|
+
let node = null;
|
|
22
|
+
if (f.startsWith('**') && f.endsWith('**') && f.length > 4) {
|
|
23
|
+
node = React.createElement(Text, { key: key, bold: true }, f.slice(2, -2));
|
|
24
|
+
}
|
|
25
|
+
else if (f.length > 2 && ((f.startsWith('*') && f.endsWith('*')) || (f.startsWith('_') && f.endsWith('_'))) &&
|
|
26
|
+
// path-güvenli: kelime/yol-ayracı bitişiğinde italik sayma (dosya_adı, a/b_c bozulmasın)
|
|
27
|
+
!/\w/.test(text.substring(m.index - 1, m.index)) &&
|
|
28
|
+
!/\w/.test(text.substring(INLINE_RE.lastIndex, INLINE_RE.lastIndex + 1)) &&
|
|
29
|
+
!/\S[./\\]/.test(text.substring(m.index - 2, m.index)) &&
|
|
30
|
+
!/[./\\]\S/.test(text.substring(INLINE_RE.lastIndex, INLINE_RE.lastIndex + 2))) {
|
|
31
|
+
node = React.createElement(Text, { key: key, italic: true }, f.slice(1, -1));
|
|
32
|
+
}
|
|
33
|
+
else if (f.startsWith('~~') && f.endsWith('~~') && f.length > 4) {
|
|
34
|
+
node = React.createElement(Text, { key: key, strikethrough: true }, f.slice(2, -2));
|
|
35
|
+
}
|
|
36
|
+
else if (f.startsWith('`') && f.endsWith('`')) {
|
|
37
|
+
const cm = f.match(/^(`+)(.+?)\1$/s);
|
|
38
|
+
if (cm && cm[2])
|
|
39
|
+
node = React.createElement(Text, { key: key, color: theme.green }, cm[2]);
|
|
40
|
+
}
|
|
41
|
+
else if (f.startsWith('[') && f.includes('](') && f.endsWith(')')) {
|
|
42
|
+
const lm = f.match(/\[(.*?)\]\((.*?)\)/);
|
|
43
|
+
if (lm)
|
|
44
|
+
node = React.createElement(Text, { key: key },
|
|
45
|
+
lm[1],
|
|
46
|
+
React.createElement(Text, { color: theme.redBright },
|
|
47
|
+
" (",
|
|
48
|
+
lm[2],
|
|
49
|
+
")"));
|
|
50
|
+
}
|
|
51
|
+
else if (f.startsWith('<u>') && f.endsWith('</u>')) {
|
|
52
|
+
node = React.createElement(Text, { key: key, underline: true }, f.slice(3, -4));
|
|
53
|
+
}
|
|
54
|
+
else if (/^https?:\/\//.test(f)) {
|
|
55
|
+
node = React.createElement(Text, { key: key, color: theme.redBright }, f);
|
|
56
|
+
}
|
|
57
|
+
nodes.push(node ?? React.createElement(Text, { key: key }, f));
|
|
58
|
+
last = INLINE_RE.lastIndex;
|
|
59
|
+
}
|
|
60
|
+
if (last < text.length)
|
|
61
|
+
nodes.push(React.createElement(Text, { key: `t${last}` }, text.slice(last)));
|
|
62
|
+
return React.createElement(Text, null, nodes);
|
|
63
|
+
}
|
|
64
|
+
// markdown'ı soyup görüntü genişliği (tablo hizalama)
|
|
65
|
+
export function plainLen(text) {
|
|
66
|
+
const clean = text
|
|
67
|
+
.replace(/\*\*(.*?)\*\*/g, '$1').replace(/\*(.*?)\*/g, '$1').replace(/_(.*?)_/g, '$1')
|
|
68
|
+
.replace(/~~(.*?)~~/g, '$1').replace(/`(.*?)`/g, '$1').replace(/<u>(.*?)<\/u>/g, '$1')
|
|
69
|
+
.replace(/\[(.*?)\]\(.*?\)/g, '$1');
|
|
70
|
+
return stringWidth(clean);
|
|
71
|
+
}
|
|
72
|
+
// ── Tablo (box-drawing) ──────────────────────────────────────────────────────
|
|
73
|
+
function Table({ headers, rows, width }) {
|
|
74
|
+
const cols = headers.map((h, i) => Math.max(plainLen(h), ...rows.map((r) => plainLen(r[i] || ''))) + 2);
|
|
75
|
+
const total = cols.reduce((s, w) => s + w + 1, 1);
|
|
76
|
+
const scale = total > width ? width / total : 1;
|
|
77
|
+
const w = cols.map((c) => Math.max(3, Math.floor(c * scale)));
|
|
78
|
+
const cell = (c, width, header) => {
|
|
79
|
+
const cw = Math.max(0, width - 2);
|
|
80
|
+
let s = c;
|
|
81
|
+
if (plainLen(c) > cw) {
|
|
82
|
+
let lo = 0, hi = c.length, best = '';
|
|
83
|
+
while (lo <= hi) {
|
|
84
|
+
const mid = (lo + hi) >> 1;
|
|
85
|
+
const cand = c.slice(0, mid);
|
|
86
|
+
if (plainLen(cand) <= cw - 1) {
|
|
87
|
+
best = cand;
|
|
88
|
+
lo = mid + 1;
|
|
89
|
+
}
|
|
90
|
+
else
|
|
91
|
+
hi = mid - 1;
|
|
92
|
+
}
|
|
93
|
+
s = best + '…';
|
|
94
|
+
}
|
|
95
|
+
const pad = ' '.repeat(Math.max(0, cw - plainLen(s)));
|
|
96
|
+
return header ? React.createElement(Text, { bold: true, color: theme.redBright },
|
|
97
|
+
React.createElement(RenderInline, { text: s }),
|
|
98
|
+
pad) : React.createElement(Text, null,
|
|
99
|
+
React.createElement(RenderInline, { text: s }),
|
|
100
|
+
pad);
|
|
101
|
+
};
|
|
102
|
+
const border = (l, mid, r) => (React.createElement(Text, { color: theme.greyDim }, l + w.map((x) => '─'.repeat(x)).join(mid) + r));
|
|
103
|
+
const row = (cells, header) => (React.createElement(Text, { color: theme.greyDim },
|
|
104
|
+
"\u2502 ",
|
|
105
|
+
w.map((x, i) => (React.createElement(React.Fragment, { key: i },
|
|
106
|
+
cell(cells[i] || '', x, header),
|
|
107
|
+
i < w.length - 1 ? ' │ ' : ''))),
|
|
108
|
+
" \u2502"));
|
|
109
|
+
return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
110
|
+
border('┌', '┬', '┐'),
|
|
111
|
+
row(headers, true),
|
|
112
|
+
border('├', '┼', '┤'),
|
|
113
|
+
rows.map((r, i) => React.createElement(React.Fragment, { key: i }, row(r, false))),
|
|
114
|
+
border('└', '┴', '┘')));
|
|
115
|
+
}
|
|
116
|
+
// ── Blok parser ──────────────────────────────────────────────────────────────
|
|
117
|
+
export function MarkdownDisplay({ text, width }) {
|
|
118
|
+
if (!text)
|
|
119
|
+
return null;
|
|
120
|
+
const W = Math.max(20, (width ?? (process.stdout.columns || 80)) - 4);
|
|
121
|
+
const lines = text.split('\n');
|
|
122
|
+
const blocks = [];
|
|
123
|
+
let key = 0;
|
|
124
|
+
const push = (n) => blocks.push(React.createElement(React.Fragment, { key: key++ }, n));
|
|
125
|
+
let inCode = false, codeFence = '', codeLines = [];
|
|
126
|
+
let inTable = false, tHeaders = [], tRows = [];
|
|
127
|
+
const flushTable = () => { if (tHeaders.length)
|
|
128
|
+
push(React.createElement(Table, { headers: tHeaders, rows: tRows, width: W })); inTable = false; tHeaders = []; tRows = []; };
|
|
129
|
+
const headerRe = /^ *(#{1,4}) +(.*)/;
|
|
130
|
+
const fenceRe = /^ *(`{3,}|~{3,}) *(\w*)? *$/;
|
|
131
|
+
const ulRe = /^([ \t]*)([-*+]) +(.*)/;
|
|
132
|
+
const olRe = /^([ \t]*)(\d+)\. +(.*)/;
|
|
133
|
+
const hrRe = /^ *([-*_] *){3,} *$/;
|
|
134
|
+
const rowRe = /^\s*\|(.+)\|\s*$/;
|
|
135
|
+
const sepRe = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$/;
|
|
136
|
+
lines.forEach((line, idx) => {
|
|
137
|
+
if (inCode) {
|
|
138
|
+
const fm = line.match(fenceRe);
|
|
139
|
+
if (fm && fm[1][0] === codeFence[0] && fm[1].length >= codeFence.length) {
|
|
140
|
+
push(React.createElement(Box, { flexDirection: "column", marginY: 1 }, codeLines.map((c, i) => React.createElement(Text, { key: i, color: theme.green }, c))));
|
|
141
|
+
inCode = false;
|
|
142
|
+
codeFence = '';
|
|
143
|
+
codeLines = [];
|
|
144
|
+
}
|
|
145
|
+
else
|
|
146
|
+
codeLines.push(line);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const fm = line.match(fenceRe);
|
|
150
|
+
const rm = line.match(rowRe);
|
|
151
|
+
if (fm) {
|
|
152
|
+
if (inTable)
|
|
153
|
+
flushTable();
|
|
154
|
+
inCode = true;
|
|
155
|
+
codeFence = fm[1];
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (rm && !inTable) {
|
|
159
|
+
if (idx + 1 < lines.length && sepRe.test(lines[idx + 1])) {
|
|
160
|
+
inTable = true;
|
|
161
|
+
tHeaders = rm[1].split('|').map((c) => c.trim());
|
|
162
|
+
tRows = [];
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
push(React.createElement(Text, { color: theme.white },
|
|
166
|
+
React.createElement(RenderInline, { text: line })));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (inTable && sepRe.test(line))
|
|
170
|
+
return;
|
|
171
|
+
if (inTable && rm) {
|
|
172
|
+
const cells = rm[1].split('|').map((c) => c.trim());
|
|
173
|
+
while (cells.length < tHeaders.length)
|
|
174
|
+
cells.push('');
|
|
175
|
+
cells.length = tHeaders.length;
|
|
176
|
+
tRows.push(cells);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (inTable && !rm)
|
|
180
|
+
flushTable();
|
|
181
|
+
const hm = line.match(headerRe);
|
|
182
|
+
const um = line.match(ulRe);
|
|
183
|
+
const om = line.match(olRe);
|
|
184
|
+
if (hrRe.test(line) && line.trim().length >= 3) {
|
|
185
|
+
push(React.createElement(Text, { color: theme.greyDim }, '─'.repeat(Math.min(W, 40))));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (hm) {
|
|
189
|
+
const lvl = hm[1].length;
|
|
190
|
+
const inner = React.createElement(RenderInline, { text: hm[2] });
|
|
191
|
+
if (lvl <= 2)
|
|
192
|
+
push(React.createElement(Text, { bold: true, color: theme.redBright }, inner));
|
|
193
|
+
else if (lvl === 3)
|
|
194
|
+
push(React.createElement(Text, { bold: true, color: theme.white }, inner));
|
|
195
|
+
else
|
|
196
|
+
push(React.createElement(Text, { italic: true, color: theme.grey }, inner));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (um || om) {
|
|
200
|
+
const [, ws, marker, itemText] = (um || om);
|
|
201
|
+
const prefix = om ? `${marker}. ` : '• ';
|
|
202
|
+
push(React.createElement(Box, { paddingLeft: ws.length },
|
|
203
|
+
React.createElement(Text, { color: theme.grey }, prefix),
|
|
204
|
+
React.createElement(Box, { flexGrow: 1 },
|
|
205
|
+
React.createElement(Text, { color: theme.white },
|
|
206
|
+
React.createElement(RenderInline, { text: itemText })))));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (line.trim().length === 0) {
|
|
210
|
+
push(React.createElement(Box, { height: 1 }));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
push(React.createElement(Text, { color: theme.white, wrap: "wrap" },
|
|
214
|
+
React.createElement(RenderInline, { text: line })));
|
|
215
|
+
});
|
|
216
|
+
if (inCode)
|
|
217
|
+
push(React.createElement(Box, { flexDirection: "column", marginY: 1 }, codeLines.map((c, i) => React.createElement(Text, { key: i, color: theme.green }, c))));
|
|
218
|
+
if (inTable)
|
|
219
|
+
flushTable();
|
|
220
|
+
return React.createElement(Box, { flexDirection: "column" }, blocks);
|
|
221
|
+
}
|
package/dist/memory.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Tetik eşikleri ve kilit mekanizması Claude Code'dan uyarlandı.
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
|
+
import * as os from 'node:os';
|
|
5
6
|
import { completeText } from './agent.js';
|
|
6
7
|
import { approxTokens } from './usage.js';
|
|
7
8
|
import { tasks } from './tasks.js';
|
|
@@ -133,6 +134,46 @@ export function dreamTimeGatePassed() {
|
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
export function getMemoryPath() { return MEM_FILE; }
|
|
137
|
+
// ── Açık kayıt: save_memory tool'u (otomatik hafızayı tamamlar) ────────────────
|
|
138
|
+
const MEMORY_HEADER = '## Eklenen Hatıralar';
|
|
139
|
+
/**
|
|
140
|
+
* Modelin/kullanıcının açıkça istediği tek bir kalıcı bilgiyi hafıza dosyasına ekler.
|
|
141
|
+
* Otomatik konsolidasyondan bağımsız; "## Eklenen Hatıralar" başlığı altına "- <fact>" yazar.
|
|
142
|
+
* scope: 'project' → cwd/.wormclaude/memory.md (varsayılan), 'global' → ~/.wormclaude/memory.md.
|
|
143
|
+
*/
|
|
144
|
+
export function saveMemoryFact(fact, scope = 'project') {
|
|
145
|
+
const clean = String(fact || '').trim().replace(/^(-+\s*)+/, '').trim();
|
|
146
|
+
if (!clean)
|
|
147
|
+
throw new Error('boş hatıra');
|
|
148
|
+
const dir = scope === 'global'
|
|
149
|
+
? path.join(os.homedir(), '.wormclaude')
|
|
150
|
+
: MEM_DIR;
|
|
151
|
+
const file = path.join(dir, 'memory.md');
|
|
152
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
153
|
+
let content = '';
|
|
154
|
+
try {
|
|
155
|
+
content = fs.readFileSync(file, 'utf8');
|
|
156
|
+
}
|
|
157
|
+
catch { }
|
|
158
|
+
const item = `- ${clean}`;
|
|
159
|
+
const idx = content.indexOf(MEMORY_HEADER);
|
|
160
|
+
if (idx === -1) {
|
|
161
|
+
const sep = content.length === 0 ? '' : content.endsWith('\n') ? '\n' : '\n\n';
|
|
162
|
+
content += `${sep}${MEMORY_HEADER}\n${item}\n`;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Başlıktan sonraki bölümün sonuna ekle (sonraki ## başlığına kadar)
|
|
166
|
+
const start = idx + MEMORY_HEADER.length;
|
|
167
|
+
let end = content.indexOf('\n## ', start);
|
|
168
|
+
if (end === -1)
|
|
169
|
+
end = content.length;
|
|
170
|
+
const before = content.slice(0, end).replace(/\s+$/, '');
|
|
171
|
+
const after = content.slice(end);
|
|
172
|
+
content = `${before}\n${item}${after.startsWith('\n') ? '' : '\n'}${after}`;
|
|
173
|
+
}
|
|
174
|
+
fs.writeFileSync(file, content);
|
|
175
|
+
return file;
|
|
176
|
+
}
|
|
136
177
|
// Kalıcı hafızayı başlangıçta context'e yüklemek için oku.
|
|
137
178
|
// .wormclaude/memory.md (oturumlar arası hafıza) + WORMCLAUDE.md (proje notu).
|
|
138
179
|
export function loadMemoryContext() {
|
package/dist/safejson.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Qwen-14B'nin bozuk JSON tool-call argümanlarını kurtarmak için dayanıklı JSON yardımcıları.
|
|
2
|
+
// Bağımlılıksız (jsonrepair YOK) — elle onarım stratejileri. Gemini safeJsonParse + fixBooleanCasing'den
|
|
3
|
+
// uyarlandı; WormClaude'a özel sadeleştirildi.
|
|
4
|
+
/**
|
|
5
|
+
* Python-tarzı True/False/None'ı JSON karşılıklarına çevirir (özyinelemeli, string-güvenli).
|
|
6
|
+
* Qwen bazen `{"flag": True}` üretir → geçerli JSON değil.
|
|
7
|
+
*/
|
|
8
|
+
export function fixBooleanCasing(text) {
|
|
9
|
+
let out = '';
|
|
10
|
+
let inString = false;
|
|
11
|
+
let escape = false;
|
|
12
|
+
for (let i = 0; i < text.length; i++) {
|
|
13
|
+
const ch = text[i];
|
|
14
|
+
if (inString) {
|
|
15
|
+
out += ch;
|
|
16
|
+
if (escape)
|
|
17
|
+
escape = false;
|
|
18
|
+
else if (ch === '\\')
|
|
19
|
+
escape = true;
|
|
20
|
+
else if (ch === '"')
|
|
21
|
+
inString = false;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (ch === '"') {
|
|
25
|
+
inString = true;
|
|
26
|
+
out += ch;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
// String dışında: Python literal'lerini kelime-sınırında değiştir
|
|
30
|
+
const rest = text.slice(i);
|
|
31
|
+
const m = /^(True|False|None)\b/.exec(rest);
|
|
32
|
+
if (m) {
|
|
33
|
+
out += m[1] === 'True' ? 'true' : m[1] === 'False' ? 'false' : 'null';
|
|
34
|
+
i += m[1].length - 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
out += ch;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** String dışındaki trailing virgülleri kaldırır: `{"a":1,}` → `{"a":1}` */
|
|
42
|
+
function stripTrailingCommas(text) {
|
|
43
|
+
let out = '';
|
|
44
|
+
let inString = false;
|
|
45
|
+
let escape = false;
|
|
46
|
+
for (let i = 0; i < text.length; i++) {
|
|
47
|
+
const ch = text[i];
|
|
48
|
+
if (inString) {
|
|
49
|
+
out += ch;
|
|
50
|
+
if (escape)
|
|
51
|
+
escape = false;
|
|
52
|
+
else if (ch === '\\')
|
|
53
|
+
escape = true;
|
|
54
|
+
else if (ch === '"')
|
|
55
|
+
inString = false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (ch === '"') {
|
|
59
|
+
inString = true;
|
|
60
|
+
out += ch;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (ch === ',') {
|
|
64
|
+
// Sonraki anlamlı karakter } veya ] ise virgülü at
|
|
65
|
+
let j = i + 1;
|
|
66
|
+
while (j < text.length && /\s/.test(text[j]))
|
|
67
|
+
j++;
|
|
68
|
+
if (text[j] === '}' || text[j] === ']')
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
out += ch;
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/** Açık kalan string/parantez/köşeli ayraçları kapatır (streaming kesintisi onarımı). */
|
|
76
|
+
function closeOpenStructures(text) {
|
|
77
|
+
let inString = false;
|
|
78
|
+
let escape = false;
|
|
79
|
+
const stack = [];
|
|
80
|
+
for (let i = 0; i < text.length; i++) {
|
|
81
|
+
const ch = text[i];
|
|
82
|
+
if (inString) {
|
|
83
|
+
if (escape)
|
|
84
|
+
escape = false;
|
|
85
|
+
else if (ch === '\\')
|
|
86
|
+
escape = true;
|
|
87
|
+
else if (ch === '"')
|
|
88
|
+
inString = false;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (ch === '"')
|
|
92
|
+
inString = true;
|
|
93
|
+
else if (ch === '{')
|
|
94
|
+
stack.push('}');
|
|
95
|
+
else if (ch === '[')
|
|
96
|
+
stack.push(']');
|
|
97
|
+
else if (ch === '}' || ch === ']')
|
|
98
|
+
stack.pop();
|
|
99
|
+
}
|
|
100
|
+
let out = text;
|
|
101
|
+
if (inString)
|
|
102
|
+
out += '"';
|
|
103
|
+
while (stack.length)
|
|
104
|
+
out += stack.pop();
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Bozuk/yarım JSON'u en iyi çabayla parse eder. Sırayla: düz parse → fixBooleanCasing →
|
|
109
|
+
* trailing-comma temizliği → açık yapıları kapatma. Hepsi başarısızsa fallback döner.
|
|
110
|
+
*/
|
|
111
|
+
export function safeJsonParse(text, fallback) {
|
|
112
|
+
if (text == null)
|
|
113
|
+
return fallback;
|
|
114
|
+
const raw = String(text).trim();
|
|
115
|
+
if (!raw)
|
|
116
|
+
return fallback;
|
|
117
|
+
// 1) Düz
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(raw);
|
|
120
|
+
}
|
|
121
|
+
catch { }
|
|
122
|
+
// 2) Boolean casing
|
|
123
|
+
let repaired = fixBooleanCasing(raw);
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(repaired);
|
|
126
|
+
}
|
|
127
|
+
catch { }
|
|
128
|
+
// 3) Trailing virgüller
|
|
129
|
+
repaired = stripTrailingCommas(repaired);
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(repaired);
|
|
132
|
+
}
|
|
133
|
+
catch { }
|
|
134
|
+
// 4) Açık yapıları kapat
|
|
135
|
+
repaired = closeOpenStructures(repaired);
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(repaired);
|
|
138
|
+
}
|
|
139
|
+
catch { }
|
|
140
|
+
// 5) Pes
|
|
141
|
+
return fallback;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Döngüsel referansları ve hata/undefined değerleri tolere ederek JSON üretir.
|
|
145
|
+
* Asla fırlatmaz; başarısızsa fallback string döner.
|
|
146
|
+
*/
|
|
147
|
+
export function safeJsonStringify(value, space, fallback = '"[unserializable]"') {
|
|
148
|
+
const seen = new WeakSet();
|
|
149
|
+
try {
|
|
150
|
+
return JSON.stringify(value, (_k, v) => {
|
|
151
|
+
if (typeof v === 'bigint')
|
|
152
|
+
return v.toString();
|
|
153
|
+
if (v instanceof Error)
|
|
154
|
+
return { name: v.name, message: v.message };
|
|
155
|
+
if (typeof v === 'object' && v !== null) {
|
|
156
|
+
if (seen.has(v))
|
|
157
|
+
return '[Circular]';
|
|
158
|
+
seen.add(v);
|
|
159
|
+
}
|
|
160
|
+
return v;
|
|
161
|
+
}, space);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return fallback;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Streaming tool-call delta'larını dayanıklı biçimde birleştirir.
|
|
2
|
+
// Qwen-14B + vLLM(hermes) parçalı JSON arg, index çakışması, ID'siz devam chunk'ı üretebilir.
|
|
3
|
+
// Gemini streamingToolCallParser'dan uyarlandı; bizim {index, id?, name?, argChunk} delta'mıza göre sade.
|
|
4
|
+
import { safeJsonParse } from './safejson.js';
|
|
5
|
+
export class StreamingToolCallParser {
|
|
6
|
+
buffers = new Map();
|
|
7
|
+
depths = new Map();
|
|
8
|
+
inStrings = new Map();
|
|
9
|
+
escapes = new Map();
|
|
10
|
+
meta = new Map();
|
|
11
|
+
idToIndex = new Map();
|
|
12
|
+
nextIndex = 0;
|
|
13
|
+
/**
|
|
14
|
+
* Bir delta chunk'ı ekler. index streaming yanıtından gelir (çakışabilir); id yeni çağrı başlangıcı.
|
|
15
|
+
* @returns chunk işlendikten sonra o index'te JSON tamamlandıysa parse edilmiş değer.
|
|
16
|
+
*/
|
|
17
|
+
addChunk(index, chunk, id, name) {
|
|
18
|
+
let actual = index;
|
|
19
|
+
if (id) {
|
|
20
|
+
if (this.idToIndex.has(id)) {
|
|
21
|
+
actual = this.idToIndex.get(id);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// Bu index'te FARKLI id'li, tamamlanmış bir çağrı varsa yeni slot bul
|
|
25
|
+
if (this.buffers.has(index)) {
|
|
26
|
+
const buf = this.buffers.get(index);
|
|
27
|
+
const depth = this.depths.get(index);
|
|
28
|
+
const m = this.meta.get(index);
|
|
29
|
+
if (buf.trim() && depth === 0 && m?.id && m.id !== id) {
|
|
30
|
+
try {
|
|
31
|
+
JSON.parse(buf);
|
|
32
|
+
actual = this.findNextIndex();
|
|
33
|
+
}
|
|
34
|
+
catch { /* yarım → güvenle yeniden kullan */ }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
this.idToIndex.set(id, actual);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (this.buffers.has(index)) {
|
|
41
|
+
// ID'siz devam: bu index tamamlandıysa en son yarım çağrıya yönlendir
|
|
42
|
+
const buf = this.buffers.get(index);
|
|
43
|
+
const depth = this.depths.get(index);
|
|
44
|
+
if (depth === 0 && buf.trim()) {
|
|
45
|
+
try {
|
|
46
|
+
JSON.parse(buf);
|
|
47
|
+
actual = this.findMostRecentIncomplete();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
actual = index;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!this.buffers.has(actual)) {
|
|
55
|
+
this.buffers.set(actual, '');
|
|
56
|
+
this.depths.set(actual, 0);
|
|
57
|
+
this.inStrings.set(actual, false);
|
|
58
|
+
this.escapes.set(actual, false);
|
|
59
|
+
this.meta.set(actual, {});
|
|
60
|
+
}
|
|
61
|
+
const m = this.meta.get(actual);
|
|
62
|
+
if (id)
|
|
63
|
+
m.id = id;
|
|
64
|
+
if (name)
|
|
65
|
+
m.name = (m.name || '') + name;
|
|
66
|
+
const newBuffer = this.buffers.get(actual) + chunk;
|
|
67
|
+
this.buffers.set(actual, newBuffer);
|
|
68
|
+
// Derinlik takibi — sadece string DIŞINDAKİ ayraçları say
|
|
69
|
+
let depth = this.depths.get(actual);
|
|
70
|
+
let inString = this.inStrings.get(actual);
|
|
71
|
+
let escape = this.escapes.get(actual);
|
|
72
|
+
for (const ch of chunk) {
|
|
73
|
+
if (!inString) {
|
|
74
|
+
if (ch === '{' || ch === '[')
|
|
75
|
+
depth++;
|
|
76
|
+
else if (ch === '}' || ch === ']')
|
|
77
|
+
depth--;
|
|
78
|
+
}
|
|
79
|
+
if (ch === '"' && !escape)
|
|
80
|
+
inString = !inString;
|
|
81
|
+
escape = ch === '\\' && !escape;
|
|
82
|
+
}
|
|
83
|
+
this.depths.set(actual, depth);
|
|
84
|
+
this.inStrings.set(actual, inString);
|
|
85
|
+
this.escapes.set(actual, escape);
|
|
86
|
+
if (depth === 0 && newBuffer.trim().length > 0) {
|
|
87
|
+
try {
|
|
88
|
+
return { complete: true, value: JSON.parse(newBuffer) };
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return { complete: false };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { complete: false };
|
|
95
|
+
}
|
|
96
|
+
/** Stream bittiğinde (finish_reason) çağrılır. name'i olan ve buffer'ı dolu tüm çağrıları döndürür. */
|
|
97
|
+
getCompleted() {
|
|
98
|
+
const out = [];
|
|
99
|
+
for (const [index, buffer] of this.buffers.entries()) {
|
|
100
|
+
const m = this.meta.get(index);
|
|
101
|
+
if (!m?.name)
|
|
102
|
+
continue; // isimsiz parça → çağrı değil
|
|
103
|
+
// Boş buffer = argümansız tool (ör. {}). safeJsonParse onarım dener, başarısızsa {} fallback.
|
|
104
|
+
const args = buffer.trim() ? safeJsonParse(buffer, {}) : {};
|
|
105
|
+
out.push({ id: m.id || `call_${index}`, name: m.name, args, index });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
reset() {
|
|
110
|
+
this.buffers.clear();
|
|
111
|
+
this.depths.clear();
|
|
112
|
+
this.inStrings.clear();
|
|
113
|
+
this.escapes.clear();
|
|
114
|
+
this.meta.clear();
|
|
115
|
+
this.idToIndex.clear();
|
|
116
|
+
this.nextIndex = 0;
|
|
117
|
+
}
|
|
118
|
+
findNextIndex() {
|
|
119
|
+
while (this.buffers.has(this.nextIndex)) {
|
|
120
|
+
const buf = this.buffers.get(this.nextIndex);
|
|
121
|
+
const depth = this.depths.get(this.nextIndex);
|
|
122
|
+
const m = this.meta.get(this.nextIndex);
|
|
123
|
+
if (!buf.trim() || depth > 0 || !m?.id)
|
|
124
|
+
return this.nextIndex;
|
|
125
|
+
try {
|
|
126
|
+
JSON.parse(buf);
|
|
127
|
+
if (depth === 0) {
|
|
128
|
+
this.nextIndex++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return this.nextIndex;
|
|
134
|
+
}
|
|
135
|
+
this.nextIndex++;
|
|
136
|
+
}
|
|
137
|
+
return this.nextIndex++;
|
|
138
|
+
}
|
|
139
|
+
findMostRecentIncomplete() {
|
|
140
|
+
let maxIndex = -1;
|
|
141
|
+
for (const [index, buffer] of this.buffers.entries()) {
|
|
142
|
+
const depth = this.depths.get(index);
|
|
143
|
+
const m = this.meta.get(index);
|
|
144
|
+
if (m?.id && (depth > 0 || !buffer.trim())) {
|
|
145
|
+
maxIndex = Math.max(maxIndex, index);
|
|
146
|
+
}
|
|
147
|
+
else if (buffer.trim()) {
|
|
148
|
+
try {
|
|
149
|
+
JSON.parse(buffer);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
maxIndex = Math.max(maxIndex, index);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return maxIndex >= 0 ? maxIndex : this.findNextIndex();
|
|
157
|
+
}
|
|
158
|
+
}
|