u-foo 2.5.5 → 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.
@@ -2,55 +2,6 @@ const { loadConfig } = require("../../config");
2
2
  const path = require("path");
3
3
  const fs = require("fs");
4
4
 
5
- function bundledModuleRoots() {
6
- return [
7
- path.join(__dirname, ".."),
8
- ];
9
- }
10
-
11
- function resolveFirstExisting(paths = []) {
12
- for (const candidate of paths) {
13
- if (!candidate) continue;
14
- try {
15
- if (fs.existsSync(candidate)) return candidate;
16
- } catch {
17
- // ignore
18
- }
19
- }
20
- return "";
21
- }
22
-
23
- function defaultBundledCoreRoot() {
24
- const root = path.join(__dirname, "..");
25
- const agentEntry = path.join(root, "agent.js");
26
- if (resolveFirstExisting([agentEntry])) return root;
27
- return root;
28
- }
29
-
30
- function defaultBundledPromptFile() {
31
- const moduleRoots = bundledModuleRoots();
32
- const candidates = moduleRoots.map((root) => path.join(root, "UCODE_PROMPT.md"));
33
- return resolveFirstExisting(candidates) || candidates[0];
34
- }
35
-
36
- function isWindowsPlatform() {
37
- return process.platform === "win32";
38
- }
39
-
40
- function canExecutePath(filePath = "") {
41
- const target = String(filePath || "").trim();
42
- if (!target) return false;
43
- try {
44
- const stat = fs.statSync(target);
45
- if (!stat.isFile()) return false;
46
- if (isWindowsPlatform()) return true;
47
- fs.accessSync(target, fs.constants.X_OK);
48
- return true;
49
- } catch {
50
- return false;
51
- }
52
- }
53
-
54
5
  function isReadableFile(filePath = "") {
55
6
  const target = String(filePath || "").trim();
56
7
  if (!target) return false;
@@ -61,113 +12,6 @@ function isReadableFile(filePath = "") {
61
12
  }
62
13
  }
63
14
 
64
- function resolveExecutableFromPath(command = "", env = process.env) {
65
- const text = String(command || "").trim();
66
- if (!text) return "";
67
- if (path.isAbsolute(text) || text.includes("/") || text.includes("\\")) {
68
- return canExecutePath(text) ? path.resolve(text) : "";
69
- }
70
-
71
- const pathText = String((env && env.PATH) || process.env.PATH || "").trim();
72
- if (!pathText) return "";
73
- const dirs = pathText.split(path.delimiter).map((item) => String(item || "").trim()).filter(Boolean);
74
- if (dirs.length === 0) return "";
75
-
76
- const hasExplicitExt = /\.[a-zA-Z0-9]+$/.test(text);
77
- const exts = isWindowsPlatform()
78
- ? String((env && env.PATHEXT) || process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM")
79
- .split(";")
80
- .map((item) => item.trim())
81
- .filter(Boolean)
82
- : [""];
83
- const suffixes = hasExplicitExt ? [""] : exts;
84
-
85
- for (const dir of dirs) {
86
- for (const ext of suffixes) {
87
- const candidate = path.join(dir, `${text}${ext}`);
88
- if (canExecutePath(candidate)) return candidate;
89
- }
90
- }
91
- return "";
92
- }
93
-
94
- function tokenizeCommand(raw = "") {
95
- const text = String(raw || "");
96
- const tokens = [];
97
- let current = "";
98
- let quote = "";
99
-
100
- for (let i = 0; i < text.length; i += 1) {
101
- const ch = text[i];
102
- if (quote) {
103
- if (quote === "\"") {
104
- if (ch === "\\") {
105
- if (i + 1 < text.length) {
106
- const next = text[i + 1];
107
- if (next === "\"" || next === "\\") {
108
- current += next;
109
- i += 1;
110
- continue;
111
- }
112
- }
113
- current += "\\";
114
- continue;
115
- }
116
- } else if (quote === "'" && ch === "\\") {
117
- current += "\\";
118
- continue;
119
- }
120
- if (ch === quote) {
121
- quote = "";
122
- continue;
123
- }
124
- current += ch;
125
- continue;
126
- }
127
-
128
- if (ch === "'" || ch === "\"") {
129
- quote = ch;
130
- continue;
131
- }
132
- if (ch === "\\") {
133
- if (i + 1 < text.length) {
134
- const next = text[i + 1];
135
- if (/\s/.test(next) || next === "'" || next === "\"" || next === "\\") {
136
- current += next;
137
- i += 1;
138
- } else {
139
- current += "\\";
140
- }
141
- } else {
142
- current += "\\";
143
- }
144
- continue;
145
- }
146
- if (/\s/.test(ch)) {
147
- if (current) {
148
- tokens.push(current);
149
- current = "";
150
- }
151
- continue;
152
- }
153
- current += ch;
154
- }
155
-
156
- if (quote) {
157
- return String(raw || "").trim().split(/\s+/).filter(Boolean);
158
- }
159
- if (current) tokens.push(current);
160
- return tokens;
161
- }
162
-
163
- function splitCommand(raw, fallback = "pi") {
164
- const text = String(raw || "").trim();
165
- if (!text) return { command: fallback, args: [] };
166
- const parts = tokenizeCommand(text);
167
- if (parts.length === 0) return { command: fallback, args: [] };
168
- return { command: parts[0], args: parts.slice(1) };
169
- }
170
-
171
15
  function hasAnyArg(args = [], names = []) {
172
16
  if (!Array.isArray(args) || args.length === 0) return false;
173
17
  const flags = new Set((Array.isArray(names) ? names : []).filter(Boolean));
@@ -182,19 +26,6 @@ function hasAnyArg(args = [], names = []) {
182
26
  });
183
27
  }
184
28
 
185
- function pickBinEntry(binField = {}) {
186
- if (typeof binField === "string" && binField.trim()) {
187
- return binField.trim();
188
- }
189
- if (!binField || typeof binField !== "object") return "";
190
- const entries = Object.entries(binField)
191
- .filter(([, value]) => typeof value === "string" && value.trim());
192
- if (entries.length === 0) return "";
193
- const preferred = entries.find(([name]) => /^(ucode|core|cli)$/i.test(String(name)));
194
- if (preferred) return preferred[1].trim();
195
- return entries[0][1].trim();
196
- }
197
-
198
29
  function normalizeAppendSystemPromptMode(value = "") {
199
30
  const text = String(value || "").trim().toLowerCase();
200
31
  if (text === "always" || text === "force" || text === "on" || text === "1" || text === "true") return "always";
@@ -202,20 +33,6 @@ function normalizeAppendSystemPromptMode(value = "") {
202
33
  return "auto";
203
34
  }
204
35
 
205
- function isLikelyPiCoreCommand(command = "", args = []) {
206
- const cmdText = String(command || "").trim();
207
- const cmdBase = path.basename(cmdText).toLowerCase();
208
- if (cmdBase === "ucode" || cmdBase === "ucode.exe") return true;
209
-
210
- const joined = [cmdText, ...(Array.isArray(args) ? args : [])]
211
- .map((part) => String(part || "").toLowerCase())
212
- .join(" ");
213
- if (!joined) return false;
214
- if (joined.includes("/src/code/agent.js")) return true;
215
- if (joined.includes("\\src\\code\\agent.js")) return true;
216
- return false;
217
- }
218
-
219
36
  function readLastArgValue(args = [], flag = "") {
220
37
  if (!Array.isArray(args) || !flag) return "";
221
38
  let value = "";
@@ -224,8 +41,10 @@ function readLastArgValue(args = [], flag = "") {
224
41
  if (!item) continue;
225
42
  if (item === flag) {
226
43
  const next = String(args[i + 1] || "").trim();
227
- if (next) value = next;
228
- i += 1;
44
+ if (next && !next.startsWith("--")) {
45
+ value = next;
46
+ i += 1;
47
+ }
229
48
  continue;
230
49
  }
231
50
  if (item.startsWith(`${flag}=`)) {
@@ -236,54 +55,6 @@ function readLastArgValue(args = [], flag = "") {
236
55
  return value;
237
56
  }
238
57
 
239
- function resolveCoreFromPath(coreRoot = "") {
240
- const requestedRoot = String(coreRoot || "").trim();
241
- if (!requestedRoot) return null;
242
- let stat;
243
- try {
244
- stat = fs.statSync(requestedRoot);
245
- } catch {
246
- return null;
247
- }
248
- if (!stat.isDirectory()) return null;
249
-
250
- const candidates = [
251
- requestedRoot,
252
- path.join(requestedRoot, "packages", "coding-agent"),
253
- ];
254
-
255
- for (const root of candidates) {
256
- const packageFile = path.join(root, "package.json");
257
- let pkg = null;
258
- try {
259
- pkg = JSON.parse(fs.readFileSync(packageFile, "utf8"));
260
- } catch {
261
- continue;
262
- }
263
- const binRel = pickBinEntry(pkg && pkg.bin ? pkg.bin : {});
264
- if (!binRel) continue;
265
- const binAbs = path.resolve(root, binRel);
266
- if (!fs.existsSync(binAbs)) continue;
267
- return {
268
- command: process.execPath,
269
- args: [binAbs],
270
- root,
271
- };
272
- }
273
- return null;
274
- }
275
-
276
- function resolveCandidateCoreRoot({
277
- env = process.env,
278
- config = {},
279
- } = {}) {
280
- // Native-only mode: external pi-mono path is no longer used as launch fallback.
281
- // Keep function for compatibility with older diagnostic surfaces.
282
- void env;
283
- void config;
284
- return null;
285
- }
286
-
287
58
  function resolveNativeFallbackCommand({ env = process.env } = {}) {
288
59
  void env;
289
60
  const entry = path.resolve(__dirname, "..", "agent.js");
@@ -342,7 +113,7 @@ function resolveUcodeLaunch({
342
113
  const promptFile = String(
343
114
  env.UFOO_UCODE_PROMPT_FILE
344
115
  || config.ucodePromptFile
345
- || defaultBundledPromptFile()
116
+ || ""
346
117
  ).trim();
347
118
  const bootstrapFile = String(
348
119
  env.UFOO_UCODE_BOOTSTRAP_FILE
@@ -359,16 +130,10 @@ function resolveUcodeLaunch({
359
130
  || config.ucodeAppendSystemPromptMode
360
131
  || "auto"
361
132
  );
133
+ // Native-only mode: the bundled native core always supports
134
+ // --append-system-prompt, so only mode=never suppresses injection.
362
135
  const hasSystemPromptArg = hasAnyArg(finalArgs, ["--system-prompt", "--append-system-prompt"]);
363
- const appendSupported = appendSystemPromptMode === "always"
364
- || (
365
- appendSystemPromptMode === "auto"
366
- && (
367
- nativeCore.kind === "native"
368
- || isLikelyPiCoreCommand(command, finalArgs)
369
- )
370
- );
371
- if (!hasSystemPromptArg && appendSystemPrompt && appendSystemPromptMode !== "never" && appendSupported) {
136
+ if (!hasSystemPromptArg && appendSystemPrompt && appendSystemPromptMode !== "never") {
372
137
  finalArgs.push("--append-system-prompt", appendSystemPrompt);
373
138
  }
374
139
  const effectiveProvider = readLastArgValue(finalArgs, "--provider");
@@ -396,20 +161,9 @@ function resolveUcodeLaunch({
396
161
  }
397
162
 
398
163
  module.exports = {
399
- bundledModuleRoots,
400
- defaultBundledCoreRoot,
401
- defaultBundledPromptFile,
402
- tokenizeCommand,
403
- splitCommand,
404
164
  hasAnyArg,
405
- pickBinEntry,
406
165
  normalizeAppendSystemPromptMode,
407
- isLikelyPiCoreCommand,
408
166
  readLastArgValue,
409
- resolveCoreFromPath,
410
- resolveCandidateCoreRoot,
411
- canExecutePath,
412
- resolveExecutableFromPath,
413
167
  resolveNativeFallbackCommand,
414
168
  resolveUcodeLaunch,
415
169
  };
@@ -86,16 +86,32 @@ function mergeDefaultUfooProtocolPrompt(projectRoot = "", promptText = "") {
86
86
  return [defaultPrompt, currentPrompt].filter(Boolean).join("\n\n");
87
87
  }
88
88
 
89
+ function isPathInsideRoot(root = "", target = "") {
90
+ const relative = path.relative(path.resolve(root), path.resolve(target));
91
+ return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
92
+ }
93
+
89
94
  function prepareUcodeBootstrap({
90
95
  projectRoot = process.cwd(),
91
96
  promptFile = "",
92
97
  promptText = "",
93
98
  targetFile = "",
94
99
  includeDefaultProtocol = true,
100
+ allowOutsideProjectRoot = false,
95
101
  } = {}) {
96
102
  const resolvedProjectRoot = path.resolve(projectRoot);
97
103
  const resolvedPrompt = String(promptFile || "").trim();
98
- const resolvedTarget = String(targetFile || "").trim() || defaultBootstrapPath(resolvedProjectRoot);
104
+ const resolvedTarget = path.resolve(
105
+ resolvedProjectRoot,
106
+ String(targetFile || "").trim() || defaultBootstrapPath(resolvedProjectRoot)
107
+ );
108
+ // The target may come from the project config (.ufoo/config.json), which is
109
+ // attacker-controlled in a cloned repository. Refuse to write outside the
110
+ // project root unless the caller marked the target as user-trusted (e.g.
111
+ // the user's own UFOO_UCODE_BOOTSTRAP_FILE env var).
112
+ if (!allowOutsideProjectRoot && !isPathInsideRoot(resolvedProjectRoot, resolvedTarget)) {
113
+ throw new Error(`ucode bootstrap target must stay inside the project root: ${resolvedTarget}`);
114
+ }
99
115
 
100
116
  const inlinePromptText = String(promptText || "").trim();
101
117
  const resolvedPromptText = inlinePromptText || readFileSafe(resolvedPrompt);
@@ -129,5 +145,6 @@ module.exports = {
129
145
  resolveProjectRules,
130
146
  defaultBootstrapPath,
131
147
  buildBootstrapContent,
148
+ isPathInsideRoot,
132
149
  prepareUcodeBootstrap,
133
150
  };
@@ -27,15 +27,12 @@ function inspectUcodeBuildSetup({
27
27
  const workspaceRoot = root;
28
28
  const distCliPath = Array.isArray(native.args) && native.args[0] ? path.resolve(native.args[0]) : "";
29
29
  const distCliExists = Boolean(distCliPath && fs.existsSync(distCliPath));
30
- const nodeModulesPath = "";
31
30
  return {
32
31
  projectRoot: root,
33
32
  coreRoot,
34
33
  workspaceRoot,
35
34
  distCliPath,
36
35
  distCliExists,
37
- nodeModulesPath,
38
- nodeModulesExists: Boolean(nodeModulesPath && fs.existsSync(nodeModulesPath)),
39
36
  };
40
37
  }
41
38
 
@@ -3,7 +3,6 @@ const path = require("path");
3
3
  const { loadConfig } = require("../../config");
4
4
  const {
5
5
  resolveNativeFallbackCommand,
6
- defaultBundledPromptFile,
7
6
  } = require("./ucode");
8
7
  const { inspectUcodeBuildSetup } = require("./ucodeBuild");
9
8
  const { inspectUcodeRuntimeConfig } = require("./ucodeRuntimeConfig");
@@ -30,7 +29,7 @@ function inspectUcodeSetup({
30
29
  const promptFile = String(
31
30
  env.UFOO_UCODE_PROMPT_FILE
32
31
  || config.ucodePromptFile
33
- || defaultBundledPromptFile()
32
+ || ""
34
33
  ).trim();
35
34
  const bootstrapFile = String(
36
35
  env.UFOO_UCODE_BOOTSTRAP_FILE
@@ -130,8 +129,11 @@ function formatUcodeDoctor(result = {}) {
130
129
  if (result.configuredCommand) {
131
130
  lines.push(`configured command override (ignored in native-only mode): ${result.configuredCommand}`);
132
131
  }
133
- lines.push(`prompt: ${result.promptFile || "(none)"}${result.promptExists ? "" : " (missing)"}`);
132
+ lines.push(`prompt: ${result.promptFile || "(none)"}${result.promptFile && !result.promptExists ? " (missing)" : ""}`);
134
133
  lines.push(`bootstrap: ${result.bootstrapFile || "(none)"}`);
134
+ if (result.bootstrapPrepared && result.bootstrapPrepared.ok === false) {
135
+ lines.push(` warning: bootstrap preparation failed: ${result.bootstrapPrepared.error || "unknown error"}`);
136
+ }
135
137
  if (result.build && result.build.coreRoot) {
136
138
  lines.push(`build: ${result.build.distCliExists ? "ready" : "missing dist"}`);
137
139
  lines.push(` core root: ${result.build.coreRoot}`);
@@ -166,11 +168,27 @@ function prepareAndInspectUcode({
166
168
  loadConfigImpl = loadConfig,
167
169
  } = {}) {
168
170
  const inspection = inspectUcodeSetup({ projectRoot, env, loadConfigImpl });
169
- const prepared = prepareUcodeBootstrap({
170
- projectRoot: inspection.projectRoot,
171
- promptFile: inspection.promptFile,
172
- targetFile: inspection.bootstrapFile,
173
- });
171
+ let prepared;
172
+ try {
173
+ prepared = prepareUcodeBootstrap({
174
+ projectRoot: inspection.projectRoot,
175
+ promptFile: inspection.promptFile,
176
+ targetFile: inspection.bootstrapFile,
177
+ // The user's own env var is trusted; a bootstrap path coming from the
178
+ // project config must stay inside the project root.
179
+ allowOutsideProjectRoot: Boolean(String(env.UFOO_UCODE_BOOTSTRAP_FILE || "").trim()),
180
+ // The native core already injects the ufoo protocol via the modular
181
+ // prompt (src/agents/prompts/native/ufoo.js); inlining it into the
182
+ // bootstrap file would append the same protocol text a second time.
183
+ includeDefaultProtocol: false,
184
+ });
185
+ } catch (err) {
186
+ prepared = {
187
+ ok: false,
188
+ file: inspection.bootstrapFile,
189
+ error: err && err.message ? err.message : "failed to prepare bootstrap",
190
+ };
191
+ }
174
192
  return {
175
193
  ...inspection,
176
194
  bootstrapPrepared: prepared,
@@ -1,5 +1,6 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
+ const { randomUUID } = require("crypto");
3
4
  const { loadGlobalUcodeConfig } = require("../../config");
4
5
 
5
6
  function readJson(filePath = "", fallback = {}) {
@@ -14,10 +15,17 @@ function readJson(filePath = "", fallback = {}) {
14
15
  }
15
16
  }
16
17
 
17
- function writeJson(filePath = "", data = {}) {
18
+ function writeJson(filePath = "", data = {}, { mode = 0 } = {}) {
18
19
  if (!filePath) return;
19
20
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
20
- fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
21
+ // Write to a temp file and rename so a crash mid-write cannot leave a
22
+ // truncated JSON file behind.
23
+ const tmpFile = `${filePath}.${process.pid}-${randomUUID()}.tmp`;
24
+ fs.writeFileSync(tmpFile, `${JSON.stringify(data, null, 2)}\n`, "utf8");
25
+ fs.renameSync(tmpFile, filePath);
26
+ if (mode) {
27
+ fs.chmodSync(filePath, mode);
28
+ }
21
29
  }
22
30
 
23
31
  function resolveRuntimeValues({
@@ -103,7 +111,8 @@ function prepareUcodeRuntimeConfig({
103
111
  type: "api_key",
104
112
  key: inspection.apiKey,
105
113
  };
106
- writeJson(inspection.authFile, auth);
114
+ // auth.json carries API keys: keep it readable only by the owner.
115
+ writeJson(inspection.authFile, auth, { mode: 0o600 });
107
116
  }
108
117
 
109
118
  if (inspection.provider && inspection.baseUrl) {