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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/agent/activityDetector.js +33 -0
  3. package/src/agent/claudeSessionFiles.js +127 -0
  4. package/src/agent/launcher.js +13 -2
  5. package/src/bus/subscriber.js +16 -3
  6. package/src/chat/commandExecutor.js +0 -1
  7. package/src/chat/commands.js +10 -2
  8. package/src/chat/daemonCoordinator.js +1 -0
  9. package/src/chat/daemonMessageRouter.js +27 -16
  10. package/src/chat/daemonReconnect.js +6 -3
  11. package/src/chat/index.js +1 -0
  12. package/src/chat/inputMath.js +175 -38
  13. package/src/chat/inputSubmitHandler.js +10 -5
  14. package/src/chat/settingsController.js +3 -1
  15. package/src/chat/text.js +6 -0
  16. package/src/code/agent.js +27 -6
  17. package/src/code/nativeRunner.js +8 -4
  18. package/src/code/prompts/actions.js +21 -0
  19. package/src/code/prompts/efficiency.js +18 -0
  20. package/src/code/prompts/environment.js +50 -0
  21. package/src/code/prompts/identity.js +20 -0
  22. package/src/code/prompts/index.js +103 -0
  23. package/src/code/prompts/safety.js +11 -0
  24. package/src/code/prompts/sections.js +60 -0
  25. package/src/code/prompts/system.js +16 -0
  26. package/src/code/prompts/tasks.js +17 -0
  27. package/src/code/prompts/toolDescriptions/bash.js +21 -0
  28. package/src/code/prompts/toolDescriptions/edit.js +16 -0
  29. package/src/code/prompts/toolDescriptions/read.js +17 -0
  30. package/src/code/prompts/toolDescriptions/write.js +16 -0
  31. package/src/code/prompts/ufoo.js +21 -0
  32. package/src/daemon/groupOrchestrator.js +97 -7
  33. package/src/daemon/index.js +53 -14
  34. package/src/daemon/nicknameScope.js +80 -0
  35. package/src/daemon/ops.js +19 -6
  36. package/src/daemon/soloBootstrap.js +15 -2
@@ -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
- return Array.from(String(value || "")).length;
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
- const lines = String(text || "").split("\n");
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 lineWidth = safeStrWidth(strWidth, line);
36
- total += Math.max(1, Math.ceil(lineWidth / width));
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 before = String(text || "").slice(0, Math.max(0, pos));
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
- function getLinePosForCol(line, targetCol, strWidth) {
58
- if (targetCol <= 0) return 0;
59
- let col = 0;
60
- let offset = 0;
61
- for (const ch of Array.from(String(line || ""))) {
62
- const w = safeStrWidth(strWidth, ch);
63
- if (col + w > targetCol) return offset;
64
- col += w;
65
- offset += ch.length;
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 offset;
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 source = String(text || "");
73
- const lines = source.split("\n");
172
+ const original = String(text || "");
173
+ const expanded = expandTabs(original);
174
+ const lines = expanded.split("\n");
74
175
  let row = 0;
75
- let pos = 0;
76
- for (const line of lines) {
77
- const lineWidth = safeStrWidth(strWidth, line);
78
- const wrappedRows = Math.max(1, Math.ceil(lineWidth / width));
79
- if (targetRow < row + wrappedRows) {
80
- const rowInLine = targetRow - row;
81
- const visualCol = rowInLine * width + Math.max(0, targetCol);
82
- return pos + getLinePosForCol(line, visualCol, strWidth);
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
- pos += line.length + 1;
85
- row += wrappedRows;
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 source.length;
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 = String(value || "").trim();
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(atTarget.message)}`
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: atTarget.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
- logMessage("user", `{white-fg}→{/white-fg} ${escapeBlessed(text)}`);
142
- renderScreen(); // Render slash command immediately
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", `{white-fg}⚙{/white-fg} Launch mode: ${next}`);
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
- const inline = readTextOrFile(appendSystemPrompt) || readTextOrFile(systemPrompt);
558
- if (inline) return clampContext(inline);
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
- const envFallback = readTextOrFile(process.env.UFOO_UCODE_APPEND_SYSTEM_PROMPT)
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
- return clampContext(envFallback);
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({ appendSystemPrompt, systemPrompt }),
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,
@@ -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: "Read a text file from workspace.",
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: "Write content to a file in workspace.",
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: "Replace text in a file in workspace.",
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: "Run one shell command in workspace.",
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 };