u-foo 2.5.6 → 2.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -102,104 +102,6 @@ function clipText(value = "", maxChars = 6000) {
102
102
  return `${text.slice(0, maxChars)}\n...[truncated]`;
103
103
  }
104
104
 
105
- function summarizeFileSnippet(file = "", content = "") {
106
- const target = String(file || "").trim();
107
- const body = String(content || "").trim();
108
- if (!body) return `${target}: empty`;
109
-
110
- if (target.toLowerCase().endsWith("package.json")) {
111
- try {
112
- const parsed = JSON.parse(body);
113
- const name = String(parsed.name || "").trim() || "(unknown)";
114
- const version = String(parsed.version || "").trim() || "(unknown)";
115
- const scripts = parsed.scripts && typeof parsed.scripts === "object"
116
- ? Object.keys(parsed.scripts).slice(0, 4)
117
- : [];
118
- const scriptText = scripts.length > 0 ? ` scripts=${scripts.join(",")}` : "";
119
- return `${target}: name=${name} version=${version}${scriptText}`;
120
- } catch {
121
- // fall through
122
- }
123
- }
124
-
125
- const lines = body
126
- .split(/\r?\n/)
127
- .map((line) => line.trim())
128
- .filter(Boolean)
129
- .slice(0, 3)
130
- .map((line) => (line.length > 120 ? `${line.slice(0, 120)}...` : line));
131
-
132
- if (lines.length === 0) return `${target}: empty`;
133
- return `${target}: ${lines.join(" | ")}`;
134
- }
135
-
136
- function extractPreflightEvidence(systemPrompt = "") {
137
- const source = String(systemPrompt || "");
138
- if (!source) return [];
139
-
140
- const results = [];
141
- const fileRegex = /File:\s*([^\n]+)\n([\s\S]*?)(?=\n---\n(?:File|Command):|$)/g;
142
- let match = fileRegex.exec(source);
143
- while (match) {
144
- const file = String(match[1] || "").trim();
145
- const content = String(match[2] || "").trim();
146
- if (file) {
147
- results.push({ kind: "file", label: file, summary: summarizeFileSnippet(file, content) });
148
- }
149
- match = fileRegex.exec(source);
150
- }
151
-
152
- const cmdRegex = /Command:\s*([^\n]+)\n([\s\S]*?)(?=\n---\n(?:File|Command):|$)/g;
153
- match = cmdRegex.exec(source);
154
- while (match) {
155
- const command = String(match[1] || "").trim();
156
- const output = String(match[2] || "").trim();
157
- if (command) {
158
- const clipped = clipText(output, 300).replace(/\s+/g, " ").trim();
159
- results.push({ kind: "command", label: command, summary: `${command}: ${clipped || "(no output)"}` });
160
- }
161
- match = cmdRegex.exec(source);
162
- }
163
-
164
- return results;
165
- }
166
-
167
- function isAnalysisPrompt(text = "") {
168
- return /(?:analy[sz]e|analysis|review|audit|status|architecture|codebase|repo|project|现状|架构|审查|分析|项目|代码库)/i.test(text);
169
- }
170
-
171
- function parseReadIntent(prompt = "") {
172
- const text = String(prompt || "");
173
- const patterns = [
174
- /(?:\bread\b|\bcat\b|查看|读取)\s+([A-Za-z0-9_./\\-]+(?:\.[A-Za-z0-9._-]+)?)/i,
175
- /([A-Za-z0-9_./\\-]+\.(?:md|txt|json|js|ts|jsx|tsx|yml|yaml|toml|sh))/i,
176
- ];
177
- for (const re of patterns) {
178
- const match = text.match(re);
179
- if (!match || !match[1]) continue;
180
- const candidate = String(match[1]).trim().replace(/[),.;:]+$/, "");
181
- if (!candidate) continue;
182
- if (candidate.length > 260) continue;
183
- return candidate;
184
- }
185
- return "";
186
- }
187
-
188
- function parseBashIntent(prompt = "") {
189
- const text = String(prompt || "").trim();
190
- if (!text) return "";
191
- if (
192
- /\b(ls|dir|tree)\b/i.test(text)
193
- || /\b(list|show)\s+(files|dirs|directories|folders)\b/i.test(text)
194
- || /列出|目录|文件列表/.test(text)
195
- ) {
196
- return "ls -la";
197
- }
198
- const cmdMatch = text.match(/(?:运行|执行|run|exec(?:ute)?)\s+`([^`]+)`/i);
199
- if (cmdMatch && cmdMatch[1]) return String(cmdMatch[1]).trim();
200
- return "";
201
- }
202
-
203
105
  function normalizeProvider(value = "") {
204
106
  const text = String(value || "").trim().toLowerCase();
205
107
  if (!text) return "";
@@ -317,6 +219,11 @@ function buildCoreToolSpecs() {
317
219
  properties: {
318
220
  path: { type: "string" },
319
221
  content: { type: "string" },
222
+ mode: {
223
+ type: "string",
224
+ enum: ["overwrite", "append"],
225
+ description: 'Write mode: "overwrite" replaces the file (default), "append" adds to its end.',
226
+ },
320
227
  append: { type: "boolean" },
321
228
  },
322
229
  required: ["path", "content"],
@@ -599,6 +506,7 @@ async function runOpenAiLikeTurn({
599
506
  const announcedToolNames = new Set();
600
507
  let nextSyntheticIndex = 0;
601
508
  let lastSyntheticIndex = -1;
509
+ let sawDone = false;
602
510
 
603
511
  while (true) {
604
512
  const { done, value } = await reader.read();
@@ -612,8 +520,11 @@ async function runOpenAiLikeTurn({
612
520
  const payloadText = parseSseDataBlock(block);
613
521
  if (!payloadText) continue;
614
522
  if (payloadText === "[DONE]") {
615
- rawBuffer = "";
616
- break;
523
+ // Stop reading after this batch, but keep the buffered tail and
524
+ // finish the blocks already parsed alongside [DONE] instead of
525
+ // silently dropping them.
526
+ sawDone = true;
527
+ continue;
617
528
  }
618
529
 
619
530
  const chunk = parseJsonSafe(payloadText, null);
@@ -691,6 +602,8 @@ async function runOpenAiLikeTurn({
691
602
  }
692
603
  }
693
604
  }
605
+
606
+ if (sawDone) break;
694
607
  }
695
608
 
696
609
  if (rawBuffer.trim()) {
@@ -839,6 +752,9 @@ async function runAnthropicTurn({
839
752
  const blockMap = new Map();
840
753
  let rawBuffer = "";
841
754
  let responseText = "";
755
+ let nextSyntheticBlockIndex = 0;
756
+ let lastBlockIndex = -1;
757
+ let sawDone = false;
842
758
 
843
759
  while (true) {
844
760
  const { done, value } = await reader.read();
@@ -850,7 +766,13 @@ async function runAnthropicTurn({
850
766
 
851
767
  for (const rawBlock of parsed.blocks) {
852
768
  const { event, data } = parseSseEventBlock(rawBlock);
853
- if (!data || data === "[DONE]") continue;
769
+ if (!data) continue;
770
+ if (data === "[DONE]") {
771
+ // Stop reading after this batch instead of waiting for the server
772
+ // to close the connection, mirroring the OpenAI transport.
773
+ sawDone = true;
774
+ continue;
775
+ }
854
776
 
855
777
  const payloadChunk = parseJsonSafe(data, null);
856
778
  if (!payloadChunk || typeof payloadChunk !== "object") continue;
@@ -863,7 +785,18 @@ async function runAnthropicTurn({
863
785
  }
864
786
 
865
787
  if (event === "content_block_start") {
866
- const index = Number.isFinite(payloadChunk.index) ? payloadChunk.index : 0;
788
+ let index;
789
+ if (Number.isFinite(payloadChunk.index)) {
790
+ index = payloadChunk.index;
791
+ } else {
792
+ // Provider omitted index: each start opens a new block, so give
793
+ // it its own synthetic index instead of collapsing every block
794
+ // into slot 0.
795
+ while (blockMap.has(nextSyntheticBlockIndex)) nextSyntheticBlockIndex += 1;
796
+ index = nextSyntheticBlockIndex;
797
+ nextSyntheticBlockIndex += 1;
798
+ }
799
+ lastBlockIndex = index;
867
800
  const contentBlock = payloadChunk.content_block && typeof payloadChunk.content_block === "object"
868
801
  ? payloadChunk.content_block
869
802
  : {};
@@ -900,7 +833,15 @@ async function runAnthropicTurn({
900
833
  }
901
834
 
902
835
  if (event === "content_block_delta") {
903
- const index = Number.isFinite(payloadChunk.index) ? payloadChunk.index : 0;
836
+ let index;
837
+ if (Number.isFinite(payloadChunk.index)) {
838
+ index = payloadChunk.index;
839
+ } else if (lastBlockIndex >= 0) {
840
+ // No index: continuation of the most recently started block.
841
+ index = lastBlockIndex;
842
+ } else {
843
+ index = 0;
844
+ }
904
845
  const delta = payloadChunk.delta && typeof payloadChunk.delta === "object"
905
846
  ? payloadChunk.delta
906
847
  : {};
@@ -943,6 +884,8 @@ async function runAnthropicTurn({
943
884
  }
944
885
  }
945
886
  }
887
+
888
+ if (sawDone) break;
946
889
  }
947
890
 
948
891
  const assistantContent = Array.from(blockMap.values())
@@ -1303,6 +1246,17 @@ async function runNativeAgentTask({
1303
1246
  const guards = createGuards({ signal, timeoutMs });
1304
1247
  const nextSessionId = String(sessionId || "").trim() || `native-${randomUUID()}`;
1305
1248
  const promptText = String(prompt || "").trim();
1249
+ // Track every text delta so the error path can return the partial output
1250
+ // the model already produced instead of discarding it.
1251
+ let partialOutput = "";
1252
+ const trackingStreamDelta = (chunk) => {
1253
+ const text = String(chunk || "");
1254
+ if (!text) return;
1255
+ partialOutput += text;
1256
+ if (typeof onStreamDelta === "function") {
1257
+ onStreamDelta(chunk);
1258
+ }
1259
+ };
1306
1260
 
1307
1261
  try {
1308
1262
  guards.ensureActive();
@@ -1336,7 +1290,7 @@ async function runNativeAgentTask({
1336
1290
  baseUrl: runtime.baseUrl,
1337
1291
  apiKey: runtime.apiKey,
1338
1292
  timeoutMs,
1339
- onStreamDelta,
1293
+ onStreamDelta: trackingStreamDelta,
1340
1294
  onThinkingDelta,
1341
1295
  onPhase,
1342
1296
  onToolEvent,
@@ -1356,14 +1310,16 @@ async function runNativeAgentTask({
1356
1310
  output: outputText,
1357
1311
  messages: cloneMessageList(runResult.messages),
1358
1312
  sessionId: nextSessionId,
1359
- streamed: Boolean(runResult.streamed),
1313
+ // The loop marks streamed=true whenever it receives a stream callback;
1314
+ // only report it when the caller actually registered one.
1315
+ streamed: Boolean(runResult.streamed) && typeof onStreamDelta === "function",
1360
1316
  };
1361
1317
  } catch (err) {
1362
1318
  const message = err && err.message ? err.message : "native runner failed";
1363
1319
  return {
1364
1320
  ok: false,
1365
1321
  error: message,
1366
- output: "",
1322
+ output: partialOutput.trim(),
1367
1323
  sessionId: nextSessionId,
1368
1324
  streamed: false,
1369
1325
  };
@@ -1372,9 +1328,6 @@ async function runNativeAgentTask({
1372
1328
 
1373
1329
  module.exports = {
1374
1330
  runNativeAgentTask,
1375
- parseReadIntent,
1376
- parseBashIntent,
1377
- extractPreflightEvidence,
1378
1331
  resolveRuntimeConfig,
1379
1332
  resolveCompletionUrl,
1380
1333
  resolveAnthropicMessagesUrl,