u-foo 1.8.5 → 1.8.8
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/package.json +1 -1
- package/src/agent/activityDetector.js +33 -0
- package/src/agent/claudeSessionFiles.js +127 -0
- package/src/agent/launcher.js +13 -2
- package/src/bus/subscriber.js +16 -3
- package/src/chat/commandExecutor.js +0 -1
- package/src/chat/commands.js +10 -2
- package/src/chat/daemonCoordinator.js +1 -0
- package/src/chat/daemonMessageRouter.js +27 -16
- package/src/chat/daemonReconnect.js +6 -3
- package/src/chat/index.js +1 -0
- package/src/chat/inputMath.js +175 -38
- package/src/chat/inputSubmitHandler.js +10 -5
- package/src/chat/settingsController.js +3 -1
- package/src/chat/text.js +6 -0
- package/src/code/agent.js +27 -6
- package/src/code/nativeRunner.js +8 -4
- package/src/code/prompts/actions.js +21 -0
- package/src/code/prompts/efficiency.js +18 -0
- package/src/code/prompts/environment.js +50 -0
- package/src/code/prompts/identity.js +20 -0
- package/src/code/prompts/index.js +103 -0
- package/src/code/prompts/safety.js +11 -0
- package/src/code/prompts/sections.js +60 -0
- package/src/code/prompts/system.js +16 -0
- package/src/code/prompts/tasks.js +17 -0
- package/src/code/prompts/toolDescriptions/bash.js +21 -0
- package/src/code/prompts/toolDescriptions/edit.js +16 -0
- package/src/code/prompts/toolDescriptions/read.js +17 -0
- package/src/code/prompts/toolDescriptions/write.js +16 -0
- package/src/code/prompts/ufoo.js +21 -0
- package/src/daemon/groupOrchestrator.js +97 -7
- package/src/daemon/index.js +53 -14
- package/src/daemon/nicknameScope.js +80 -0
- package/src/daemon/ops.js +19 -6
- package/src/daemon/soloBootstrap.js +15 -2
package/src/chat/inputMath.js
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
|
+
const TAB_WIDTH = 4;
|
|
2
|
+
const TAB_REPLACEMENT = " ".repeat(TAB_WIDTH);
|
|
3
|
+
|
|
4
|
+
function expandTabs(value) {
|
|
5
|
+
return String(value || "").replace(/\t/g, TAB_REPLACEMENT);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert a cursor offset in the original text to an offset in the
|
|
10
|
+
* tab-expanded text.
|
|
11
|
+
*/
|
|
12
|
+
function originalToExpanded(text, pos) {
|
|
13
|
+
let origIdx = 0;
|
|
14
|
+
let expIdx = 0;
|
|
15
|
+
const str = String(text || "");
|
|
16
|
+
while (origIdx < pos && origIdx < str.length) {
|
|
17
|
+
if (str[origIdx] === "\t") {
|
|
18
|
+
expIdx += TAB_WIDTH;
|
|
19
|
+
} else {
|
|
20
|
+
expIdx += str[origIdx].length > 1 ? str[origIdx].length : 1;
|
|
21
|
+
}
|
|
22
|
+
origIdx += 1;
|
|
23
|
+
}
|
|
24
|
+
return expIdx;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert a cursor offset in the tab-expanded text back to an offset
|
|
29
|
+
* in the original text.
|
|
30
|
+
*/
|
|
31
|
+
function expandedToOriginal(text, expPos) {
|
|
32
|
+
let origIdx = 0;
|
|
33
|
+
let expIdx = 0;
|
|
34
|
+
const str = String(text || "");
|
|
35
|
+
while (expIdx < expPos && origIdx < str.length) {
|
|
36
|
+
if (str[origIdx] === "\t") {
|
|
37
|
+
expIdx += TAB_WIDTH;
|
|
38
|
+
} else {
|
|
39
|
+
expIdx += 1;
|
|
40
|
+
}
|
|
41
|
+
origIdx += 1;
|
|
42
|
+
}
|
|
43
|
+
return origIdx;
|
|
44
|
+
}
|
|
45
|
+
|
|
1
46
|
function safeStrWidth(strWidth, value) {
|
|
2
47
|
if (typeof strWidth === "function") return strWidth(value);
|
|
3
|
-
|
|
48
|
+
// Fallback: expand tabs to 4 spaces for width calculation
|
|
49
|
+
const expanded = expandTabs(value);
|
|
50
|
+
return Array.from(expanded).length;
|
|
4
51
|
}
|
|
5
52
|
|
|
6
53
|
function getInnerWidth({ input, screen, promptWidth = 2 }) {
|
|
@@ -27,64 +74,154 @@ function getWrapWidth(input, fallbackWidth) {
|
|
|
27
74
|
return Math.max(1, fallbackWidth || 1);
|
|
28
75
|
}
|
|
29
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Simulate blessed's wrapping for a single logical line (no newlines).
|
|
79
|
+
* Returns { rows, lastCol }.
|
|
80
|
+
*
|
|
81
|
+
* Blessed preprocesses double-width chars by inserting a \x03 marker after
|
|
82
|
+
* each one, then wraps at character count = width. This means a double-width
|
|
83
|
+
* char at col (width - 1) overflows by 1 cell — only the \x03 marker wraps
|
|
84
|
+
* to the next line. We replicate this behavior so cursor math matches what
|
|
85
|
+
* blessed actually renders.
|
|
86
|
+
*/
|
|
87
|
+
function wrapLine(line, width, strWidth) {
|
|
88
|
+
const chars = Array.from(String(line || ""));
|
|
89
|
+
let col = 0;
|
|
90
|
+
let rows = 1;
|
|
91
|
+
for (const ch of chars) {
|
|
92
|
+
const w = safeStrWidth(strWidth, ch);
|
|
93
|
+
if (w === 0) continue;
|
|
94
|
+
if (col >= width) {
|
|
95
|
+
rows += 1;
|
|
96
|
+
col = col - width;
|
|
97
|
+
}
|
|
98
|
+
col += w;
|
|
99
|
+
}
|
|
100
|
+
if (col > width) {
|
|
101
|
+
rows += 1;
|
|
102
|
+
col = col - width;
|
|
103
|
+
} else if (col === width) {
|
|
104
|
+
rows += 1;
|
|
105
|
+
col = 0;
|
|
106
|
+
}
|
|
107
|
+
return { rows, lastCol: col };
|
|
108
|
+
}
|
|
109
|
+
|
|
30
110
|
function countLines(text, width, strWidth) {
|
|
31
111
|
if (width <= 0) return 1;
|
|
32
|
-
|
|
112
|
+
// Expand tabs to match blessed's preprocessing
|
|
113
|
+
const expanded = expandTabs(text);
|
|
114
|
+
const lines = expanded.split("\n");
|
|
33
115
|
let total = 0;
|
|
34
116
|
for (const line of lines) {
|
|
35
|
-
const
|
|
36
|
-
total +=
|
|
117
|
+
const { rows, lastCol } = wrapLine(line, width, strWidth);
|
|
118
|
+
total += (lastCol === 0 && rows > 1) ? rows - 1 : rows;
|
|
37
119
|
}
|
|
38
120
|
return total;
|
|
39
121
|
}
|
|
40
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Convert a cursor offset (pos) in the original text to a visual { row, col }.
|
|
125
|
+
* Expands tabs first to match blessed's wrapping, then simulates wrapping.
|
|
126
|
+
*/
|
|
41
127
|
function getCursorRowCol(text, pos, width, strWidth) {
|
|
42
128
|
if (width <= 0) return { row: 0, col: 0 };
|
|
43
|
-
const
|
|
129
|
+
const original = String(text || "");
|
|
130
|
+
const expanded = expandTabs(original);
|
|
131
|
+
const expPos = originalToExpanded(original, Math.max(0, pos));
|
|
132
|
+
const before = expanded.slice(0, Math.max(0, expPos));
|
|
44
133
|
const lines = before.split("\n");
|
|
45
134
|
let row = 0;
|
|
46
|
-
for (let i = 0; i < lines.length - 1; i += 1) {
|
|
47
|
-
const lineWidth = safeStrWidth(strWidth, lines[i]);
|
|
48
|
-
row += Math.max(1, Math.ceil(lineWidth / width));
|
|
49
|
-
}
|
|
50
|
-
const lastLine = lines[lines.length - 1] || "";
|
|
51
|
-
const lastWidth = safeStrWidth(strWidth, lastLine);
|
|
52
|
-
row += Math.floor(lastWidth / width);
|
|
53
|
-
const col = lastWidth % width;
|
|
54
|
-
return { row, col };
|
|
55
|
-
}
|
|
56
135
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
136
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
137
|
+
const line = lines[i] || "";
|
|
138
|
+
if (i < lines.length - 1) {
|
|
139
|
+
const { rows, lastCol } = wrapLine(line, width, strWidth);
|
|
140
|
+
row += (lastCol === 0 && rows > 1) ? rows - 1 : rows;
|
|
141
|
+
} else {
|
|
142
|
+
const chars = Array.from(line);
|
|
143
|
+
let col = 0;
|
|
144
|
+
for (const ch of chars) {
|
|
145
|
+
const w = safeStrWidth(strWidth, ch);
|
|
146
|
+
if (w === 0) continue;
|
|
147
|
+
if (col >= width) {
|
|
148
|
+
row += 1;
|
|
149
|
+
col = col - width;
|
|
150
|
+
}
|
|
151
|
+
col += w;
|
|
152
|
+
}
|
|
153
|
+
if (col > width) {
|
|
154
|
+
row += 1;
|
|
155
|
+
col = col - width;
|
|
156
|
+
} else if (col === width) {
|
|
157
|
+
row += 1;
|
|
158
|
+
col = 0;
|
|
159
|
+
}
|
|
160
|
+
return { row, col };
|
|
161
|
+
}
|
|
66
162
|
}
|
|
67
|
-
return
|
|
163
|
+
return { row, col: 0 };
|
|
68
164
|
}
|
|
69
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Convert a visual { targetRow, targetCol } back to a cursor offset in
|
|
168
|
+
* the original text. Expands tabs first to match blessed's wrapping.
|
|
169
|
+
*/
|
|
70
170
|
function getCursorPosForRowCol(text, targetRow, targetCol, width, strWidth) {
|
|
71
171
|
if (width <= 0) return 0;
|
|
72
|
-
const
|
|
73
|
-
const
|
|
172
|
+
const original = String(text || "");
|
|
173
|
+
const expanded = expandTabs(original);
|
|
174
|
+
const lines = expanded.split("\n");
|
|
74
175
|
let row = 0;
|
|
75
|
-
let
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
176
|
+
let expPos = 0;
|
|
177
|
+
|
|
178
|
+
for (let li = 0; li < lines.length; li += 1) {
|
|
179
|
+
const line = lines[li];
|
|
180
|
+
const chars = Array.from(line);
|
|
181
|
+
let col = 0;
|
|
182
|
+
let lineOffset = 0;
|
|
183
|
+
|
|
184
|
+
for (const ch of chars) {
|
|
185
|
+
const w = safeStrWidth(strWidth, ch);
|
|
186
|
+
if (w === 0) {
|
|
187
|
+
lineOffset += ch.length;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (col >= width) {
|
|
191
|
+
row += 1;
|
|
192
|
+
col = col - width;
|
|
193
|
+
}
|
|
194
|
+
if (row === targetRow && col + w > targetCol) {
|
|
195
|
+
return expandedToOriginal(original, expPos + lineOffset);
|
|
196
|
+
}
|
|
197
|
+
col += w;
|
|
198
|
+
lineOffset += ch.length;
|
|
199
|
+
if (col > width) {
|
|
200
|
+
if (row === targetRow) {
|
|
201
|
+
return expandedToOriginal(original, expPos + lineOffset);
|
|
202
|
+
}
|
|
203
|
+
row += 1;
|
|
204
|
+
col = col - width;
|
|
205
|
+
} else if (col === width) {
|
|
206
|
+
if (row === targetRow) {
|
|
207
|
+
return expandedToOriginal(original, expPos + lineOffset);
|
|
208
|
+
}
|
|
209
|
+
row += 1;
|
|
210
|
+
col = 0;
|
|
211
|
+
}
|
|
83
212
|
}
|
|
84
|
-
|
|
85
|
-
row
|
|
213
|
+
|
|
214
|
+
if (row === targetRow) {
|
|
215
|
+
return expandedToOriginal(original, expPos + lineOffset);
|
|
216
|
+
}
|
|
217
|
+
if (row > targetRow) {
|
|
218
|
+
return expandedToOriginal(original, expPos + lineOffset);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
expPos += line.length + 1;
|
|
222
|
+
row += 1;
|
|
86
223
|
}
|
|
87
|
-
return
|
|
224
|
+
return original.length;
|
|
88
225
|
}
|
|
89
226
|
|
|
90
227
|
function normalizePaste(text) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
|
|
2
|
+
const { decodeEscapedNewlines } = require("./text");
|
|
3
|
+
const { shouldEchoCommandInChat } = require("./commands");
|
|
2
4
|
|
|
3
5
|
function createInputSubmitHandler(options = {}) {
|
|
4
6
|
const {
|
|
@@ -69,7 +71,7 @@ function createInputSubmitHandler(options = {}) {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
async function handleSubmit(value) {
|
|
72
|
-
const text =
|
|
74
|
+
const text = decodeEscapedNewlines(value).trim();
|
|
73
75
|
|
|
74
76
|
if (!text) {
|
|
75
77
|
if (state.targetAgent) {
|
|
@@ -120,16 +122,17 @@ function createInputSubmitHandler(options = {}) {
|
|
|
120
122
|
return;
|
|
121
123
|
}
|
|
122
124
|
const resolvedTarget = resolveAgentId(atTarget.target) || atTarget.target;
|
|
125
|
+
const message = atTarget.message.trim();
|
|
123
126
|
logMessage(
|
|
124
127
|
"user",
|
|
125
|
-
`{cyan-fg}→{/cyan-fg} {magenta-fg}@${escapeBlessed(atTarget.target)}{/magenta-fg} ${escapeBlessed(
|
|
128
|
+
`{cyan-fg}→{/cyan-fg} {magenta-fg}@${escapeBlessed(atTarget.target)}{/magenta-fg} ${escapeBlessed(message)}`
|
|
126
129
|
);
|
|
127
130
|
renderScreen(); // Immediately render the user message
|
|
128
131
|
markPendingDelivery(resolvedTarget);
|
|
129
132
|
send({
|
|
130
133
|
type: IPC_REQUEST_TYPES.BUS_SEND,
|
|
131
134
|
target: resolvedTarget,
|
|
132
|
-
message
|
|
135
|
+
message,
|
|
133
136
|
injection_mode: "immediate",
|
|
134
137
|
source: "chat-direct",
|
|
135
138
|
});
|
|
@@ -138,8 +141,10 @@ function createInputSubmitHandler(options = {}) {
|
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
if (text.startsWith("/")) {
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
if (shouldEchoCommandInChat(text)) {
|
|
145
|
+
logMessage("user", `{white-fg}→{/white-fg} ${escapeBlessed(text)}`);
|
|
146
|
+
renderScreen(); // Render slash command immediately
|
|
147
|
+
}
|
|
143
148
|
try {
|
|
144
149
|
await executeCommand(text);
|
|
145
150
|
} catch (err) {
|
|
@@ -9,6 +9,7 @@ function createSettingsController(options = {}) {
|
|
|
9
9
|
fsModule,
|
|
10
10
|
getUfooPaths = () => ({ agentDir: "" }),
|
|
11
11
|
logMessage = () => {},
|
|
12
|
+
resolveStatusLine = null,
|
|
12
13
|
renderDashboard = () => {},
|
|
13
14
|
renderScreen = () => {},
|
|
14
15
|
restartDaemon = () => {},
|
|
@@ -59,7 +60,8 @@ function createSettingsController(options = {}) {
|
|
|
59
60
|
const nextIndex = modeOptions.findIndex((opt) => opt === next);
|
|
60
61
|
setSelectedModeIndex(nextIndex >= 0 ? nextIndex : 0);
|
|
61
62
|
saveConfig(projectRoot, { launchMode: next });
|
|
62
|
-
logMessage("status",
|
|
63
|
+
const statusMsg = resolveStatusLine || ((text) => logMessage("status", text));
|
|
64
|
+
statusMsg(`{gray-fg}⚙{/gray-fg} Launch mode: ${next}`);
|
|
63
65
|
renderDashboard();
|
|
64
66
|
renderScreen();
|
|
65
67
|
void restartDaemon();
|
package/src/chat/text.js
CHANGED
|
@@ -17,6 +17,11 @@ function escapeBlessed(text) {
|
|
|
17
17
|
return `{escape}${safe}{/escape}`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function decodeEscapedNewlines(text) {
|
|
21
|
+
if (text == null) return "";
|
|
22
|
+
return String(text).replace(/\\n/g, "\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
function stripBlessedTags(text) {
|
|
21
26
|
if (text == null) return "";
|
|
22
27
|
return String(text).replace(/\{[^}]+\}/g, "");
|
|
@@ -63,6 +68,7 @@ function truncateText(text, maxWidth) {
|
|
|
63
68
|
module.exports = {
|
|
64
69
|
neutralizeBlessedCommaTags,
|
|
65
70
|
escapeBlessed,
|
|
71
|
+
decodeEscapedNewlines,
|
|
66
72
|
stripBlessedTags,
|
|
67
73
|
stripAnsi,
|
|
68
74
|
truncateAnsi,
|
package/src/code/agent.js
CHANGED
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
saveSessionSnapshot,
|
|
25
25
|
loadSessionSnapshot,
|
|
26
26
|
} = require("./sessionStore");
|
|
27
|
+
const { buildPromptContext } = require("./prompts");
|
|
27
28
|
|
|
28
29
|
function printPrompt() {
|
|
29
30
|
process.stdout.write("> ");
|
|
@@ -553,14 +554,28 @@ function formatNlResult(result, asJson = false) {
|
|
|
553
554
|
function buildNlContext({
|
|
554
555
|
appendSystemPrompt = "",
|
|
555
556
|
systemPrompt = "",
|
|
557
|
+
workspaceRoot = "",
|
|
558
|
+
model = "",
|
|
559
|
+
provider = "",
|
|
556
560
|
} = {}) {
|
|
557
|
-
|
|
558
|
-
|
|
561
|
+
// Legacy override: if caller passes a raw systemPrompt string/file, honor it
|
|
562
|
+
const override = readTextOrFile(systemPrompt);
|
|
563
|
+
if (override) return clampContext(override);
|
|
559
564
|
|
|
560
|
-
|
|
565
|
+
// Resolve append from args or env
|
|
566
|
+
const append = readTextOrFile(appendSystemPrompt)
|
|
567
|
+
|| readTextOrFile(process.env.UFOO_UCODE_APPEND_SYSTEM_PROMPT)
|
|
561
568
|
|| readTextOrFile(process.env.UFOO_UCODE_BOOTSTRAP_FILE)
|
|
562
|
-
|| readTextOrFile(process.env.UFOO_UCODE_PROMPT_FILE)
|
|
563
|
-
|
|
569
|
+
|| readTextOrFile(process.env.UFOO_UCODE_PROMPT_FILE)
|
|
570
|
+
|| "";
|
|
571
|
+
|
|
572
|
+
// New modular prompt assembly
|
|
573
|
+
return clampContext(buildPromptContext({
|
|
574
|
+
workspaceRoot: workspaceRoot || process.cwd(),
|
|
575
|
+
model,
|
|
576
|
+
provider,
|
|
577
|
+
appendSystemPrompt: append,
|
|
578
|
+
}));
|
|
564
579
|
}
|
|
565
580
|
|
|
566
581
|
function buildSessionSnapshotFromState(state = {}) {
|
|
@@ -1327,7 +1342,13 @@ async function runUcodeCoreAgent({
|
|
|
1327
1342
|
provider: resolvedUcode.provider,
|
|
1328
1343
|
model: resolvedUcode.model,
|
|
1329
1344
|
engine: "ufoo-core",
|
|
1330
|
-
context: buildNlContext({
|
|
1345
|
+
context: buildNlContext({
|
|
1346
|
+
appendSystemPrompt,
|
|
1347
|
+
systemPrompt,
|
|
1348
|
+
workspaceRoot: resolvedWorkspaceRoot,
|
|
1349
|
+
model: resolvedUcode.model,
|
|
1350
|
+
provider: resolvedUcode.provider,
|
|
1351
|
+
}),
|
|
1331
1352
|
nlMessages: [],
|
|
1332
1353
|
sessionId: resolveSessionId(String(sessionId || "").trim()),
|
|
1333
1354
|
timeoutMs,
|
package/src/code/nativeRunner.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const { randomUUID } = require("crypto");
|
|
2
2
|
const { loadConfig } = require("../config");
|
|
3
3
|
const { runToolCall } = require("./dispatch");
|
|
4
|
+
const { getReadToolDescription } = require("./prompts/toolDescriptions/read");
|
|
5
|
+
const { getWriteToolDescription } = require("./prompts/toolDescriptions/write");
|
|
6
|
+
const { getEditToolDescription } = require("./prompts/toolDescriptions/edit");
|
|
7
|
+
const { getBashToolDescription } = require("./prompts/toolDescriptions/bash");
|
|
4
8
|
|
|
5
9
|
const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash"]);
|
|
6
10
|
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
@@ -244,7 +248,7 @@ function buildCoreToolSpecs() {
|
|
|
244
248
|
type: "function",
|
|
245
249
|
function: {
|
|
246
250
|
name: "read",
|
|
247
|
-
description:
|
|
251
|
+
description: getReadToolDescription(),
|
|
248
252
|
parameters: {
|
|
249
253
|
type: "object",
|
|
250
254
|
properties: {
|
|
@@ -261,7 +265,7 @@ function buildCoreToolSpecs() {
|
|
|
261
265
|
type: "function",
|
|
262
266
|
function: {
|
|
263
267
|
name: "write",
|
|
264
|
-
description:
|
|
268
|
+
description: getWriteToolDescription(),
|
|
265
269
|
parameters: {
|
|
266
270
|
type: "object",
|
|
267
271
|
properties: {
|
|
@@ -277,7 +281,7 @@ function buildCoreToolSpecs() {
|
|
|
277
281
|
type: "function",
|
|
278
282
|
function: {
|
|
279
283
|
name: "edit",
|
|
280
|
-
description:
|
|
284
|
+
description: getEditToolDescription(),
|
|
281
285
|
parameters: {
|
|
282
286
|
type: "object",
|
|
283
287
|
properties: {
|
|
@@ -294,7 +298,7 @@ function buildCoreToolSpecs() {
|
|
|
294
298
|
type: "function",
|
|
295
299
|
function: {
|
|
296
300
|
name: "bash",
|
|
297
|
-
description:
|
|
301
|
+
description: getBashToolDescription(),
|
|
298
302
|
parameters: {
|
|
299
303
|
type: "object",
|
|
300
304
|
properties: {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function getActionsSection() {
|
|
4
|
+
return `# Executing actions with care
|
|
5
|
+
|
|
6
|
+
Carefully consider the reversibility and blast radius of actions. File reads and edits are local and reversible. But bash commands can be destructive and hard to undo — think before running them.
|
|
7
|
+
|
|
8
|
+
Actions that warrant extra caution:
|
|
9
|
+
- Destructive operations: deleting files/branches, dropping database tables, rm -rf, overwriting uncommitted changes.
|
|
10
|
+
- Hard-to-reverse operations: force-pushing, git reset --hard, amending published commits, removing packages.
|
|
11
|
+
- Actions visible to others: pushing code, creating/closing PRs or issues, sending messages to external services.
|
|
12
|
+
|
|
13
|
+
For git operations:
|
|
14
|
+
- Prefer creating new commits over amending existing ones.
|
|
15
|
+
- Never skip hooks (--no-verify) unless the user explicitly asks.
|
|
16
|
+
- Never force-push to main/master without explicit user confirmation.
|
|
17
|
+
|
|
18
|
+
When you encounter an obstacle, do not use destructive actions as a shortcut. Investigate before deleting or overwriting — unexpected files or branches may represent the user's in-progress work. When in doubt, ask before acting.`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { getActionsSection };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function getOutputEfficiencySection() {
|
|
4
|
+
return `# Output efficiency
|
|
5
|
+
|
|
6
|
+
Go straight to the point. Try the simplest approach first without going in circles.
|
|
7
|
+
|
|
8
|
+
Keep your text output brief and direct. Lead with the answer or action, not the reasoning. Skip filler words, preamble, and unnecessary transitions. Do not restate what the user said — just do it.
|
|
9
|
+
|
|
10
|
+
Focus text output on:
|
|
11
|
+
- Decisions that need the user's input.
|
|
12
|
+
- High-level status updates at natural milestones.
|
|
13
|
+
- Errors or blockers that change the plan.
|
|
14
|
+
|
|
15
|
+
If you can say it in one sentence, don't use three. Use deterministic, machine-consumable action patterns when applicable. This does not apply to code or tool calls.`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { getOutputEfficiencySection };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
|
|
6
|
+
function getIsGit(workspaceRoot) {
|
|
7
|
+
try {
|
|
8
|
+
const result = execSync("git rev-parse --is-inside-work-tree", {
|
|
9
|
+
cwd: workspaceRoot || process.cwd(),
|
|
10
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11
|
+
timeout: 3000,
|
|
12
|
+
});
|
|
13
|
+
return String(result).trim() === "true";
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getShellName() {
|
|
20
|
+
const shell = process.env.SHELL || "";
|
|
21
|
+
if (shell.includes("zsh")) return "zsh";
|
|
22
|
+
if (shell.includes("bash")) return "bash";
|
|
23
|
+
if (shell.includes("fish")) return "fish";
|
|
24
|
+
return shell || "unknown";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getEnvironmentSection({ workspaceRoot = "", model = "", provider = "" } = {}) {
|
|
28
|
+
const cwd = workspaceRoot || process.cwd();
|
|
29
|
+
const isGit = getIsGit(cwd);
|
|
30
|
+
const platform = process.platform;
|
|
31
|
+
const shell = getShellName();
|
|
32
|
+
const osInfo = `${os.type()} ${os.release()}`;
|
|
33
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
34
|
+
|
|
35
|
+
const lines = [
|
|
36
|
+
`Working directory: ${cwd}`,
|
|
37
|
+
`Is git repository: ${isGit ? "yes" : "no"}`,
|
|
38
|
+
`Platform: ${platform}`,
|
|
39
|
+
`Shell: ${shell}`,
|
|
40
|
+
`OS: ${osInfo}`,
|
|
41
|
+
`Date: ${date}`,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
if (provider) lines.push(`Provider: ${provider}`);
|
|
45
|
+
if (model) lines.push(`Model: ${model}`);
|
|
46
|
+
|
|
47
|
+
return `# Environment\n${lines.map((l) => ` - ${l}`).join("\n")}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { getEnvironmentSection, getIsGit };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const SAFETY_INSTRUCTION = `Assist with authorized security testing, defensive security, and educational contexts. Refuse requests for destructive techniques, malicious code generation, or attacks targeting real systems without explicit authorization.`;
|
|
4
|
+
|
|
5
|
+
function getIdentitySection() {
|
|
6
|
+
return `You are \`ucode\`, the ufoo coding agent core — a software engineering assistant that helps users with code tasks using the tools available to you.
|
|
7
|
+
|
|
8
|
+
Objectives:
|
|
9
|
+
- Deliver coding capability on par with leading coding agents.
|
|
10
|
+
- Integrate natively with the ufoo multi-agent ecosystem.
|
|
11
|
+
|
|
12
|
+
${SAFETY_INSTRUCTION}
|
|
13
|
+
|
|
14
|
+
IMPORTANT: Never generate or guess URLs unless you are confident they help the user with programming. You may use URLs provided by the user in their messages or local files.`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
SAFETY_INSTRUCTION,
|
|
19
|
+
getIdentitySection,
|
|
20
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { getIdentitySection } = require("./identity");
|
|
4
|
+
const { getSystemSection } = require("./system");
|
|
5
|
+
const { getDoingTasksSection } = require("./tasks");
|
|
6
|
+
const { getActionsSection } = require("./actions");
|
|
7
|
+
const { getSafetySection } = require("./safety");
|
|
8
|
+
const { getOutputEfficiencySection } = require("./efficiency");
|
|
9
|
+
const { getUfooIntegrationSection } = require("./ufoo");
|
|
10
|
+
const { getEnvironmentSection } = require("./environment");
|
|
11
|
+
const {
|
|
12
|
+
systemPromptSection,
|
|
13
|
+
resolveSections,
|
|
14
|
+
clearSectionCache,
|
|
15
|
+
} = require("./sections");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Boundary marker separating static (cacheable) content from dynamic content.
|
|
19
|
+
* Everything BEFORE this marker can be cached across turns.
|
|
20
|
+
* Everything AFTER contains session-specific content.
|
|
21
|
+
*/
|
|
22
|
+
const SYSTEM_PROMPT_DYNAMIC_BOUNDARY = "__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Assemble the full system prompt as a string array.
|
|
26
|
+
*
|
|
27
|
+
* Priority system (3 levels):
|
|
28
|
+
* 1. overrideSystemPrompt — completely replaces everything
|
|
29
|
+
* 2. Default — modular sections assembled below
|
|
30
|
+
* 3. appendSystemPrompt — always appended at the end
|
|
31
|
+
*
|
|
32
|
+
* @param {object} options
|
|
33
|
+
* @param {string} [options.workspaceRoot]
|
|
34
|
+
* @param {string} [options.model]
|
|
35
|
+
* @param {string} [options.provider]
|
|
36
|
+
* @param {string} [options.appendSystemPrompt]
|
|
37
|
+
* @param {string} [options.overrideSystemPrompt]
|
|
38
|
+
* @returns {string[]}
|
|
39
|
+
*/
|
|
40
|
+
function getSystemPrompt({
|
|
41
|
+
workspaceRoot = "",
|
|
42
|
+
model = "",
|
|
43
|
+
provider = "",
|
|
44
|
+
appendSystemPrompt = "",
|
|
45
|
+
overrideSystemPrompt = "",
|
|
46
|
+
} = {}) {
|
|
47
|
+
// Priority 1: override replaces everything
|
|
48
|
+
if (overrideSystemPrompt) {
|
|
49
|
+
return [overrideSystemPrompt];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- Static sections (cacheable, computed once per session) ---
|
|
53
|
+
const staticSections = [
|
|
54
|
+
getIdentitySection(),
|
|
55
|
+
getSystemSection(),
|
|
56
|
+
getDoingTasksSection(),
|
|
57
|
+
getActionsSection(),
|
|
58
|
+
getSafetySection(),
|
|
59
|
+
getOutputEfficiencySection(),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// --- Dynamic boundary ---
|
|
63
|
+
const boundary = SYSTEM_PROMPT_DYNAMIC_BOUNDARY;
|
|
64
|
+
|
|
65
|
+
// --- Dynamic sections (may change per session/turn) ---
|
|
66
|
+
const dynamicSectionDefs = [
|
|
67
|
+
systemPromptSection("ufoo", () => getUfooIntegrationSection()),
|
|
68
|
+
systemPromptSection("environment", () =>
|
|
69
|
+
getEnvironmentSection({ workspaceRoot, model, provider }),
|
|
70
|
+
),
|
|
71
|
+
];
|
|
72
|
+
const dynamicSections = resolveSections(dynamicSectionDefs);
|
|
73
|
+
|
|
74
|
+
// Assemble
|
|
75
|
+
const result = [
|
|
76
|
+
...staticSections,
|
|
77
|
+
boundary,
|
|
78
|
+
...dynamicSections,
|
|
79
|
+
appendSystemPrompt || null,
|
|
80
|
+
].filter((s) => s != null && s !== "");
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build a single prompt context string from the modular sections.
|
|
87
|
+
* This is the main entry point for backward compatibility with agent.js.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} options — same as getSystemPrompt
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
function buildPromptContext(options = {}) {
|
|
93
|
+
return getSystemPrompt(options)
|
|
94
|
+
.filter((s) => s !== SYSTEM_PROMPT_DYNAMIC_BOUNDARY)
|
|
95
|
+
.join("\n\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
getSystemPrompt,
|
|
100
|
+
buildPromptContext,
|
|
101
|
+
clearSectionCache,
|
|
102
|
+
SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
|
|
103
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function getSafetySection() {
|
|
4
|
+
return `# Safety
|
|
5
|
+
- Never output secrets, API keys, passwords, or credentials in your responses. If you encounter them in files, mention their presence without revealing the values.
|
|
6
|
+
- Do not commit files that likely contain secrets (.env, credentials.json, etc). Warn the user if they specifically request it.
|
|
7
|
+
- Refuse requests to generate malicious code, exploits targeting real systems, or code designed to cause harm.
|
|
8
|
+
- Be aware of workspace path boundaries — all file operations are scoped to the workspace root.`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = { getSafetySection };
|