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.
- package/bin/ucode.js +9 -0
- package/package.json +1 -1
- package/src/agents/launch/notifier.js +6 -0
- package/src/agents/prompts/native/index.js +1 -1
- package/src/agents/prompts/native/toolDescriptions/edit.js +1 -0
- package/src/code/agent.js +53 -1076
- package/src/code/busConsumer.js +504 -0
- package/src/code/dispatch.js +1 -6
- package/src/code/launcher/ucode.js +3 -251
- package/src/code/launcher/ucodeBootstrap.js +18 -1
- package/src/code/launcher/ucodeBuild.js +0 -3
- package/src/code/launcher/ucodeDoctor.js +24 -9
- package/src/code/launcher/ucodeRuntimeConfig.js +12 -3
- package/src/code/nativeRunner.js +62 -109
- package/src/code/repl.js +610 -0
- package/src/code/sessionStore.js +5 -1
- package/src/code/skills/injection.js +17 -1
- package/src/code/taskDecomposer.js +36 -29
- package/src/code/tools/common.js +34 -0
- package/src/code/tools/edit.js +11 -3
- package/src/coordination/bus/inject.js +52 -7
- package/src/coordination/bus/subscriber.js +33 -6
- package/src/runtime/daemon/deliveryScheduler.js +102 -2
- package/src/runtime/daemon/index.js +8 -1
- package/src/runtime/daemon/ops.js +23 -0
package/src/code/repl.js
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
const readline = require("readline");
|
|
2
|
+
const { runToolCall, TOOL_NAMES } = require("./dispatch");
|
|
3
|
+
const {
|
|
4
|
+
runUcodeTui,
|
|
5
|
+
shouldUseUcodeTui,
|
|
6
|
+
buildUcodeBannerLines,
|
|
7
|
+
StreamBuffer,
|
|
8
|
+
createEscapeTagStripper,
|
|
9
|
+
stripLeakedEscapeTags,
|
|
10
|
+
} = require("./tui");
|
|
11
|
+
const { stripBlessedTags } = require("../app/chat/text");
|
|
12
|
+
const { resolveSessionId } = require("./sessionStore");
|
|
13
|
+
const {
|
|
14
|
+
formatSkillsList,
|
|
15
|
+
listUcodeSkills,
|
|
16
|
+
showSkill,
|
|
17
|
+
} = require("./skills");
|
|
18
|
+
const {
|
|
19
|
+
runUbusCommand,
|
|
20
|
+
resolveUfooProjectRoot,
|
|
21
|
+
getPendingBusCount,
|
|
22
|
+
shouldAutoConsumeBus,
|
|
23
|
+
} = require("./busConsumer");
|
|
24
|
+
|
|
25
|
+
function printPrompt(stdout = process.stdout) {
|
|
26
|
+
stdout.write("> ");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function printUcodeBanner(stdout = process.stdout, { model = "", workspaceRoot = process.cwd(), sessionId = "" } = {}) {
|
|
30
|
+
stdout.write(`${buildUcodeBannerLines({
|
|
31
|
+
model,
|
|
32
|
+
engine: "ufoo-core",
|
|
33
|
+
workspaceRoot,
|
|
34
|
+
sessionId,
|
|
35
|
+
width: (stdout && stdout.columns) || 0,
|
|
36
|
+
}).join("\n")}\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeLine(input = "") {
|
|
40
|
+
return String(input || "").trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseLegacyUfooMarkerCommand(input = "") {
|
|
44
|
+
const text = String(input || "").trim();
|
|
45
|
+
if (!text) return "";
|
|
46
|
+
// Old daemons injected strict "<prefix> <single-token>" commands for
|
|
47
|
+
// session discovery. Keep ignoring those inputs after removing injection.
|
|
48
|
+
const match = text.match(/^(?:\$ufoo|\/ufoo|ufoo)\s+([A-Za-z0-9][A-Za-z0-9._:-]{0,63})$/);
|
|
49
|
+
return match ? String(match[1] || "").trim() : "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseJson(text = "") {
|
|
53
|
+
const raw = String(text || "").trim();
|
|
54
|
+
if (!raw) return {};
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractAgentNickname(agentId = "") {
|
|
61
|
+
// Extract nickname from agent ID like "ufoo-agent:abc123" -> "ufoo"
|
|
62
|
+
const id = String(agentId || "").trim();
|
|
63
|
+
if (!id) return "";
|
|
64
|
+
|
|
65
|
+
// Remove the instance ID part (after colon)
|
|
66
|
+
const base = id.split(":")[0];
|
|
67
|
+
|
|
68
|
+
// Common agent nickname mappings
|
|
69
|
+
if (base === "ufoo-agent") return "ufoo";
|
|
70
|
+
if (base === "claude-code") return "claude";
|
|
71
|
+
if (base === "ufoo-code") return "ucode";
|
|
72
|
+
|
|
73
|
+
// Return base name as-is for others
|
|
74
|
+
return base;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
78
|
+
const text = normalizeLine(line);
|
|
79
|
+
if (!text) return { kind: "empty" };
|
|
80
|
+
if (text === "exit" || text === "quit") return { kind: "exit" };
|
|
81
|
+
if (text === "help") {
|
|
82
|
+
return {
|
|
83
|
+
kind: "help",
|
|
84
|
+
output: [
|
|
85
|
+
"Commands:",
|
|
86
|
+
" help",
|
|
87
|
+
" exit|quit",
|
|
88
|
+
" ubus|/ubus",
|
|
89
|
+
" skills [list]",
|
|
90
|
+
" skills show <name>",
|
|
91
|
+
" bg|/bg <task>",
|
|
92
|
+
" resume <session-id>",
|
|
93
|
+
" tool <read|write|edit|bash> <args-json>",
|
|
94
|
+
" run <read|write|edit|bash> <args-json>",
|
|
95
|
+
].join("\n"),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const legacyUfooMarker = parseLegacyUfooMarkerCommand(text);
|
|
99
|
+
if (legacyUfooMarker) {
|
|
100
|
+
return {
|
|
101
|
+
kind: "legacy_ufoo_marker",
|
|
102
|
+
marker: legacyUfooMarker,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (text === "ubus" || text === "/ubus") {
|
|
106
|
+
return {
|
|
107
|
+
kind: "ubus",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const skillsMatch = text.match(/^(?:\/skills|skills)(?:\s+(.*))?$/i);
|
|
111
|
+
if (skillsMatch) {
|
|
112
|
+
const args = String(skillsMatch[1] || "").trim().split(/\s+/).filter(Boolean);
|
|
113
|
+
const action = String(args[0] || "list").toLowerCase();
|
|
114
|
+
if (action === "list" || action === "ls") {
|
|
115
|
+
const outcome = listUcodeSkills({ workspaceRoot });
|
|
116
|
+
return {
|
|
117
|
+
kind: "skills",
|
|
118
|
+
output: formatSkillsList(outcome),
|
|
119
|
+
skills: outcome.skills,
|
|
120
|
+
errors: outcome.errors,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (action === "show") {
|
|
124
|
+
const name = String(args[1] || "").trim();
|
|
125
|
+
if (!name) {
|
|
126
|
+
return {
|
|
127
|
+
kind: "error",
|
|
128
|
+
output: "usage: skills show <name>",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const result = showSkill({ name, workspaceRoot });
|
|
132
|
+
if (!result.ok) {
|
|
133
|
+
return {
|
|
134
|
+
kind: "error",
|
|
135
|
+
output: result.error,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
kind: "skills",
|
|
140
|
+
output: result.output,
|
|
141
|
+
skill: result.skill,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
kind: "error",
|
|
146
|
+
output: "usage: skills [list] | skills show <name>",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (text === "bg" || text === "/bg") {
|
|
150
|
+
return {
|
|
151
|
+
kind: "error",
|
|
152
|
+
output: "usage: bg <task>",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const bgMatch = text.match(/^(?:\/bg|bg)\s+(.+)$/i);
|
|
156
|
+
if (bgMatch) {
|
|
157
|
+
const task = String(bgMatch[1] || "").trim();
|
|
158
|
+
if (!task) {
|
|
159
|
+
return {
|
|
160
|
+
kind: "error",
|
|
161
|
+
output: "usage: bg <task>",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
kind: "nl_bg",
|
|
166
|
+
task,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const resumeMatch = text.match(/^resume(?:\s+(.+))?$/i);
|
|
170
|
+
if (resumeMatch) {
|
|
171
|
+
const session = String(resumeMatch[1] || "").trim();
|
|
172
|
+
if (!session) {
|
|
173
|
+
return {
|
|
174
|
+
kind: "error",
|
|
175
|
+
output: "usage: resume <session-id>",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
kind: "resume",
|
|
180
|
+
sessionId: session,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const match = text.match(/^(tool|run)\s+([a-zA-Z_-]+)\s*(.*)$/);
|
|
185
|
+
if (!match) {
|
|
186
|
+
return {
|
|
187
|
+
kind: "nl",
|
|
188
|
+
task: text,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const tool = String(match[2] || "").trim().toLowerCase();
|
|
192
|
+
if (String(match[1]).toLowerCase() === "run" && !TOOL_NAMES.includes(tool)) {
|
|
193
|
+
// Natural language like "run the tests" is not a tool invocation.
|
|
194
|
+
return {
|
|
195
|
+
kind: "nl",
|
|
196
|
+
task: text,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const payload = String(match[3] || "").trim();
|
|
200
|
+
let args = {};
|
|
201
|
+
try {
|
|
202
|
+
args = parseJson(payload);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
return {
|
|
205
|
+
kind: "error",
|
|
206
|
+
output: JSON.stringify({ ok: false, error: err && err.message ? err.message : "invalid json" }),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const result = runToolCall(
|
|
210
|
+
{ tool, args },
|
|
211
|
+
{ workspaceRoot, cwd: workspaceRoot }
|
|
212
|
+
);
|
|
213
|
+
return {
|
|
214
|
+
kind: "tool",
|
|
215
|
+
tool,
|
|
216
|
+
args,
|
|
217
|
+
result,
|
|
218
|
+
output: JSON.stringify(result),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function runUcodeCoreAgent({
|
|
223
|
+
stdin = process.stdin,
|
|
224
|
+
stdout = process.stdout,
|
|
225
|
+
workspaceRoot = process.cwd(),
|
|
226
|
+
provider = "",
|
|
227
|
+
model = "",
|
|
228
|
+
appendSystemPrompt = "",
|
|
229
|
+
systemPrompt = "",
|
|
230
|
+
sessionId = "",
|
|
231
|
+
timeoutMs = 600000,
|
|
232
|
+
jsonOutput = false,
|
|
233
|
+
forceTui = false,
|
|
234
|
+
disableTui = false,
|
|
235
|
+
} = {}) {
|
|
236
|
+
// Lazy-required to avoid a circular require with ./agent (nl orchestration),
|
|
237
|
+
// which re-exports this module.
|
|
238
|
+
const {
|
|
239
|
+
buildNlContext,
|
|
240
|
+
formatNlResult,
|
|
241
|
+
persistSessionState,
|
|
242
|
+
resumeSessionState,
|
|
243
|
+
resolveUcodeProviderModel,
|
|
244
|
+
runNaturalLanguageTask,
|
|
245
|
+
} = require("./agent");
|
|
246
|
+
const resolvedWorkspaceRoot = resolveUfooProjectRoot(workspaceRoot);
|
|
247
|
+
const resolvedUcode = resolveUcodeProviderModel({
|
|
248
|
+
workspaceRoot: resolvedWorkspaceRoot,
|
|
249
|
+
provider,
|
|
250
|
+
model,
|
|
251
|
+
});
|
|
252
|
+
const state = {
|
|
253
|
+
workspaceRoot: resolvedWorkspaceRoot,
|
|
254
|
+
provider: resolvedUcode.provider,
|
|
255
|
+
model: resolvedUcode.model,
|
|
256
|
+
engine: "ufoo-core",
|
|
257
|
+
context: buildNlContext({
|
|
258
|
+
appendSystemPrompt,
|
|
259
|
+
systemPrompt,
|
|
260
|
+
workspaceRoot: resolvedWorkspaceRoot,
|
|
261
|
+
model: resolvedUcode.model,
|
|
262
|
+
provider: resolvedUcode.provider,
|
|
263
|
+
}),
|
|
264
|
+
nlMessages: [],
|
|
265
|
+
sessionId: resolveSessionId(String(sessionId || "").trim()),
|
|
266
|
+
timeoutMs,
|
|
267
|
+
jsonOutput,
|
|
268
|
+
};
|
|
269
|
+
persistSessionState(state);
|
|
270
|
+
|
|
271
|
+
if (shouldUseUcodeTui({
|
|
272
|
+
stdin,
|
|
273
|
+
stdout,
|
|
274
|
+
jsonOutput,
|
|
275
|
+
forceTui,
|
|
276
|
+
disableTui: disableTui || process.env.UFOO_UCODE_NO_TUI === "1",
|
|
277
|
+
})) {
|
|
278
|
+
return runUcodeTui({
|
|
279
|
+
stdin,
|
|
280
|
+
stdout,
|
|
281
|
+
runSingleCommand,
|
|
282
|
+
runNaturalLanguageTask,
|
|
283
|
+
runUbusCommand,
|
|
284
|
+
formatNlResult,
|
|
285
|
+
workspaceRoot,
|
|
286
|
+
state,
|
|
287
|
+
resumeSessionState,
|
|
288
|
+
persistSessionState,
|
|
289
|
+
autoBus: {
|
|
290
|
+
enabled: shouldAutoConsumeBus(process.env.UFOO_SUBSCRIBER_ID || ""),
|
|
291
|
+
getPendingCount: () => getPendingBusCount(state.workspaceRoot || workspaceRoot, process.env.UFOO_SUBSCRIBER_ID || ""),
|
|
292
|
+
subscriberId: String(process.env.UFOO_SUBSCRIBER_ID || "").trim(),
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
printUcodeBanner(stdout, {
|
|
298
|
+
model: state.model || "default",
|
|
299
|
+
workspaceRoot: workspaceRoot,
|
|
300
|
+
sessionId: state.sessionId,
|
|
301
|
+
});
|
|
302
|
+
printPrompt(stdout);
|
|
303
|
+
const rl = readline.createInterface({
|
|
304
|
+
input: stdin,
|
|
305
|
+
output: stdout,
|
|
306
|
+
terminal: true,
|
|
307
|
+
historySize: 200,
|
|
308
|
+
});
|
|
309
|
+
return new Promise((resolve) => {
|
|
310
|
+
let chain = Promise.resolve();
|
|
311
|
+
let backgroundSeq = 0;
|
|
312
|
+
const backgroundRuns = new Map();
|
|
313
|
+
const subscriberId = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
|
|
314
|
+
const autoBusEnabled = shouldAutoConsumeBus(subscriberId);
|
|
315
|
+
let autoBusTimer = null;
|
|
316
|
+
let autoBusQueued = false;
|
|
317
|
+
let autoBusError = "";
|
|
318
|
+
let closing = false;
|
|
319
|
+
|
|
320
|
+
const runAutoBusOnce = async () => {
|
|
321
|
+
if (!autoBusEnabled || closing) return;
|
|
322
|
+
if (getPendingBusCount(state.workspaceRoot || workspaceRoot, subscriberId) <= 0) {
|
|
323
|
+
autoBusError = "";
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const ubusResult = await runUbusCommand(state, {
|
|
327
|
+
workspaceRoot: state.workspaceRoot || workspaceRoot,
|
|
328
|
+
subscriberId,
|
|
329
|
+
});
|
|
330
|
+
if (!ubusResult.ok) {
|
|
331
|
+
const nextError = String(ubusResult.error || "ubus failed");
|
|
332
|
+
if (nextError !== autoBusError) {
|
|
333
|
+
autoBusError = nextError;
|
|
334
|
+
stdout.write(`Error: ${nextError}\n`);
|
|
335
|
+
printPrompt(stdout);
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
autoBusError = "";
|
|
340
|
+
if (ubusResult.handled > 0) {
|
|
341
|
+
const persisted = persistSessionState(state);
|
|
342
|
+
if (!persisted || persisted.ok === false) {
|
|
343
|
+
stdout.write(`Warning: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}\n`);
|
|
344
|
+
printPrompt(stdout);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const scheduleAutoBus = () => {
|
|
350
|
+
if (!autoBusEnabled || closing || autoBusQueued) return;
|
|
351
|
+
if (getPendingBusCount(state.workspaceRoot || workspaceRoot, subscriberId) <= 0) return;
|
|
352
|
+
autoBusQueued = true;
|
|
353
|
+
chain = chain
|
|
354
|
+
.then(() => runAutoBusOnce())
|
|
355
|
+
.catch(() => {})
|
|
356
|
+
.finally(() => {
|
|
357
|
+
autoBusQueued = false;
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
if (autoBusEnabled) {
|
|
362
|
+
autoBusTimer = setInterval(() => {
|
|
363
|
+
scheduleAutoBus();
|
|
364
|
+
}, 800);
|
|
365
|
+
scheduleAutoBus();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const startBackgroundTask = (task = "") => {
|
|
369
|
+
backgroundSeq += 1;
|
|
370
|
+
const jobId = `bg-${Date.now().toString(36)}-${backgroundSeq.toString(36)}`;
|
|
371
|
+
const bgState = {
|
|
372
|
+
workspaceRoot: state.workspaceRoot,
|
|
373
|
+
provider: state.provider,
|
|
374
|
+
model: state.model,
|
|
375
|
+
engine: state.engine,
|
|
376
|
+
context: state.context,
|
|
377
|
+
nlMessages: Array.isArray(state.nlMessages) ? state.nlMessages.slice() : [],
|
|
378
|
+
sessionId: "",
|
|
379
|
+
timeoutMs: state.timeoutMs,
|
|
380
|
+
jsonOutput: false,
|
|
381
|
+
};
|
|
382
|
+
const run = runNaturalLanguageTask(task, bgState)
|
|
383
|
+
.then((nlResult) => {
|
|
384
|
+
const summary = String(formatNlResult(nlResult, false) || "").trim();
|
|
385
|
+
const title = nlResult && nlResult.ok ? "done" : "failed";
|
|
386
|
+
stdout.write(`[${jobId}] ${title}: ${summary || "no summary"}\n`);
|
|
387
|
+
printPrompt(stdout);
|
|
388
|
+
})
|
|
389
|
+
.catch((err) => {
|
|
390
|
+
stdout.write(`[${jobId}] failed: ${err && err.message ? err.message : "background task failed"}\n`);
|
|
391
|
+
printPrompt(stdout);
|
|
392
|
+
})
|
|
393
|
+
.finally(() => {
|
|
394
|
+
backgroundRuns.delete(jobId);
|
|
395
|
+
});
|
|
396
|
+
backgroundRuns.set(jobId, run);
|
|
397
|
+
return jobId;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const handleLine = async (line) => {
|
|
401
|
+
const runtimeWorkspace = String(state.workspaceRoot || workspaceRoot || process.cwd());
|
|
402
|
+
const result = runSingleCommand(line, runtimeWorkspace);
|
|
403
|
+
if (result.kind === "exit") {
|
|
404
|
+
rl.close();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (result.kind === "legacy_ufoo_marker") {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (result.kind === "help" || result.kind === "tool" || result.kind === "skills" || result.kind === "error") {
|
|
411
|
+
stdout.write(`${result.output}\n`);
|
|
412
|
+
}
|
|
413
|
+
if (result.kind === "ubus") {
|
|
414
|
+
const ubusResult = await runUbusCommand(state, {
|
|
415
|
+
workspaceRoot: runtimeWorkspace,
|
|
416
|
+
onMessageReceived: (msg) => {
|
|
417
|
+
// Display the incoming message immediately
|
|
418
|
+
const nickname = extractAgentNickname(msg.from) || msg.from;
|
|
419
|
+
stdout.write(`${nickname}: ${msg.task}\n`);
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
if (!ubusResult.ok) {
|
|
423
|
+
stdout.write(`Error: ${ubusResult.error}\n`);
|
|
424
|
+
} else {
|
|
425
|
+
// Display replies for each message
|
|
426
|
+
if (ubusResult.messageExchanges && ubusResult.messageExchanges.length > 0) {
|
|
427
|
+
for (const exchange of ubusResult.messageExchanges) {
|
|
428
|
+
const nickname = extractAgentNickname(exchange.from) || exchange.from;
|
|
429
|
+
stdout.write(`@${nickname} ${exchange.reply}\n`);
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
stdout.write(`${ubusResult.summary}\n`);
|
|
433
|
+
}
|
|
434
|
+
persistSessionState(state);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (result.kind === "resume") {
|
|
438
|
+
const resumed = resumeSessionState(state, result.sessionId, state.workspaceRoot || resolvedWorkspaceRoot);
|
|
439
|
+
if (!resumed.ok) {
|
|
440
|
+
stdout.write(`Error: ${resumed.error}\n`);
|
|
441
|
+
} else {
|
|
442
|
+
stdout.write(`Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).\n`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (result.kind === "nl_bg") {
|
|
446
|
+
const jobId = startBackgroundTask(result.task);
|
|
447
|
+
stdout.write(`[${jobId}] started in background.\n`);
|
|
448
|
+
}
|
|
449
|
+
if (result.kind === "nl") {
|
|
450
|
+
let streamBuffer = null;
|
|
451
|
+
let streamedVisible = false;
|
|
452
|
+
const escapeStripper = createEscapeTagStripper();
|
|
453
|
+
if (!state.jsonOutput) {
|
|
454
|
+
streamBuffer = new StreamBuffer(stdout.write.bind(stdout), {
|
|
455
|
+
delay: 10,
|
|
456
|
+
chunkSize: 4,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const nlResult = await runNaturalLanguageTask(result.task, state, {
|
|
461
|
+
onDelta: state.jsonOutput
|
|
462
|
+
? null
|
|
463
|
+
: async (delta) => {
|
|
464
|
+
const text = escapeStripper.write(String(delta || ""));
|
|
465
|
+
const safeText = stripBlessedTags(stripLeakedEscapeTags(text));
|
|
466
|
+
if (!safeText) return;
|
|
467
|
+
if (/[^\s]/.test(safeText)) {
|
|
468
|
+
streamedVisible = true;
|
|
469
|
+
}
|
|
470
|
+
if (streamBuffer) {
|
|
471
|
+
await streamBuffer.write(safeText);
|
|
472
|
+
} else {
|
|
473
|
+
stdout.write(safeText);
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
if (!state.jsonOutput) {
|
|
479
|
+
const tail = escapeStripper.flush();
|
|
480
|
+
const safeTail = stripBlessedTags(stripLeakedEscapeTags(tail));
|
|
481
|
+
if (safeTail) {
|
|
482
|
+
if (/[^\s]/.test(safeTail)) {
|
|
483
|
+
streamedVisible = true;
|
|
484
|
+
}
|
|
485
|
+
if (streamBuffer) {
|
|
486
|
+
await streamBuffer.write(safeTail);
|
|
487
|
+
} else {
|
|
488
|
+
stdout.write(safeTail);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Ensure buffer is flushed
|
|
494
|
+
if (streamBuffer) {
|
|
495
|
+
await streamBuffer.finish();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const streamed = !state.jsonOutput && Boolean(nlResult && nlResult.streamed);
|
|
499
|
+
if (streamed && streamedVisible && nlResult && nlResult.streamLastChar !== "\n") {
|
|
500
|
+
stdout.write("\n");
|
|
501
|
+
}
|
|
502
|
+
const shouldSkipSummary = Boolean(streamed && nlResult && nlResult.ok && streamedVisible);
|
|
503
|
+
if (!shouldSkipSummary) {
|
|
504
|
+
const formatted = formatNlResult(nlResult, state.jsonOutput);
|
|
505
|
+
const safeOutput = state.jsonOutput
|
|
506
|
+
? formatted
|
|
507
|
+
: stripBlessedTags(stripLeakedEscapeTags(formatted));
|
|
508
|
+
stdout.write(`${safeOutput}\n`);
|
|
509
|
+
}
|
|
510
|
+
const persisted = persistSessionState(state);
|
|
511
|
+
if (!state.jsonOutput && (!persisted || persisted.ok === false)) {
|
|
512
|
+
stdout.write(`Warning: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}\n`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
printPrompt(stdout);
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
rl.on("line", (line) => {
|
|
519
|
+
chain = chain.then(() => handleLine(line)).catch((err) => {
|
|
520
|
+
stdout.write(`${JSON.stringify({ ok: false, error: err && err.message ? err.message : "agent loop failed" })}\n`);
|
|
521
|
+
printPrompt(stdout);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
rl.on("close", () => {
|
|
526
|
+
closing = true;
|
|
527
|
+
if (autoBusTimer) {
|
|
528
|
+
clearInterval(autoBusTimer);
|
|
529
|
+
autoBusTimer = null;
|
|
530
|
+
}
|
|
531
|
+
chain.finally(() => resolve({ code: 0 }));
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function parseAgentArgs(argv = []) {
|
|
537
|
+
const args = Array.isArray(argv) ? argv.slice() : [];
|
|
538
|
+
const out = {
|
|
539
|
+
workspaceRoot: "",
|
|
540
|
+
provider: "",
|
|
541
|
+
model: "",
|
|
542
|
+
appendSystemPrompt: "",
|
|
543
|
+
systemPrompt: "",
|
|
544
|
+
sessionId: "",
|
|
545
|
+
timeoutMs: 600000,
|
|
546
|
+
jsonOutput: false,
|
|
547
|
+
forceTui: false,
|
|
548
|
+
disableTui: false,
|
|
549
|
+
};
|
|
550
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
551
|
+
const item = String(args[i] || "").trim();
|
|
552
|
+
if (!item) continue;
|
|
553
|
+
if (item === "--workspace" || item === "--cwd") {
|
|
554
|
+
out.workspaceRoot = String(args[i + 1] || "").trim();
|
|
555
|
+
i += 1;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (item === "--provider") {
|
|
559
|
+
out.provider = String(args[i + 1] || "").trim();
|
|
560
|
+
i += 1;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (item === "--model") {
|
|
564
|
+
out.model = String(args[i + 1] || "").trim();
|
|
565
|
+
i += 1;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (item === "--append-system-prompt") {
|
|
569
|
+
out.appendSystemPrompt = String(args[i + 1] || "").trim();
|
|
570
|
+
i += 1;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (item === "--system-prompt") {
|
|
574
|
+
out.systemPrompt = String(args[i + 1] || "").trim();
|
|
575
|
+
i += 1;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (item === "--session-id") {
|
|
579
|
+
out.sessionId = String(args[i + 1] || "").trim();
|
|
580
|
+
i += 1;
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (item === "--timeout-ms") {
|
|
584
|
+
const parsed = Number(args[i + 1]);
|
|
585
|
+
if (Number.isFinite(parsed)) out.timeoutMs = Math.max(1000, Math.floor(parsed));
|
|
586
|
+
i += 1;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (item === "--json") {
|
|
590
|
+
out.jsonOutput = true;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (item === "--tui") {
|
|
594
|
+
out.forceTui = true;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (item === "--no-tui") {
|
|
598
|
+
out.disableTui = true;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return out;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
module.exports = {
|
|
606
|
+
runUcodeCoreAgent,
|
|
607
|
+
runSingleCommand,
|
|
608
|
+
extractAgentNickname,
|
|
609
|
+
parseAgentArgs,
|
|
610
|
+
};
|
package/src/code/sessionStore.js
CHANGED
|
@@ -81,7 +81,11 @@ function saveSessionSnapshot(workspaceRoot = process.cwd(), snapshot = {}) {
|
|
|
81
81
|
|
|
82
82
|
try {
|
|
83
83
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
84
|
-
|
|
84
|
+
// Write to a temp file and rename so a crash mid-write cannot leave a
|
|
85
|
+
// corrupted session JSON behind.
|
|
86
|
+
const tmpFile = `${filePath}.${process.pid}-${randomUUID()}.tmp`;
|
|
87
|
+
fs.writeFileSync(tmpFile, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
88
|
+
fs.renameSync(tmpFile, filePath);
|
|
85
89
|
return {
|
|
86
90
|
ok: true,
|
|
87
91
|
error: "",
|
|
@@ -55,8 +55,24 @@ function findSkillByPath(skills = [], targetPath = "") {
|
|
|
55
55
|
return (Array.isArray(skills) ? skills : []).find((skill) => canonicalPath(skill.path) === target) || null;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// Skill bodies are inlined into the prompt verbatim. A hostile or bloated
|
|
59
|
+
// SKILL.md could otherwise blow up the context window or close the <skill>
|
|
60
|
+
// block early to smuggle instructions, so cap the body size and neutralize
|
|
61
|
+
// any embedded closing tag. 32KB chars is a rough token budget proxy.
|
|
62
|
+
const MAX_SKILL_CONTENT_CHARS = 32 * 1024;
|
|
63
|
+
|
|
64
|
+
function sanitizeSkillContent(content = "") {
|
|
65
|
+
let text = String(content || "");
|
|
66
|
+
// Escape literal closing tags so the body cannot break out of the block.
|
|
67
|
+
text = text.replace(/<\/skill\s*>/gi, "</skill>");
|
|
68
|
+
if (text.length > MAX_SKILL_CONTENT_CHARS) {
|
|
69
|
+
text = `${text.slice(0, MAX_SKILL_CONTENT_CHARS)}\n...[skill content truncated: exceeded ${MAX_SKILL_CONTENT_CHARS} chars]`;
|
|
70
|
+
}
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
function readSkillBlock(skill) {
|
|
59
|
-
const content = fs.readFileSync(skill.path, "utf8");
|
|
75
|
+
const content = sanitizeSkillContent(fs.readFileSync(skill.path, "utf8"));
|
|
60
76
|
return `<skill>\n<name>${skill.name}</name>\n<path>${String(skill.path).replace(/\\/g, "/")}</path>\n${content}\n</skill>`;
|
|
61
77
|
}
|
|
62
78
|
|