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.
@@ -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
+ };
@@ -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
- fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
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, "&lt;/skill&gt;");
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