u-foo 2.3.19 → 2.3.20
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/README.md +8 -2
- package/README.zh-CN.md +8 -2
- package/package.json +1 -1
- package/src/chat/agentViewController.js +23 -10
- package/src/chat/commandExecutor.js +3 -3
- package/src/chat/commands.js +3 -3
- package/src/chat/daemonMessageRouter.js +18 -2
- package/src/chat/index.js +23 -0
- package/src/code/agent.js +118 -4
- package/src/code/cli.js +47 -0
- package/src/code/prompts/index.js +9 -0
- package/src/code/skills/index.js +74 -0
- package/src/code/skills/injection.js +120 -0
- package/src/code/skills/loader.js +218 -0
- package/src/code/skills/render.js +46 -0
- package/src/daemon/index.js +196 -12
- package/src/shared/eventContract.js +1 -0
package/README.md
CHANGED
|
@@ -143,7 +143,9 @@ ufoo resume <ucode|uclaude|ucodex|nickname>
|
|
|
143
143
|
ufoo recover list
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
Common chat commands include `/status`, `/bus list`, `/bus status`, `/settings`, `/project list`, `/project switch <index|path>`, `/open <path>`, `/resume list`, `/group status`, and `@nickname <message>`.
|
|
146
|
+
Common chat commands include `/status`, `/bus list`, `/bus status`, `/settings`, `/project list`, `/project switch <index|path>`, `/open <path>`, `/resume list`, `/group status`, `/skills`, and `@nickname <message>`.
|
|
147
|
+
|
|
148
|
+
In `ufoo chat`, `/skills` lists ufoo's built-in available skills and preset workflow capabilities so users can discover and choose them. It does not execute a task by itself, and it is not a private capability list for any one agent.
|
|
147
149
|
|
|
148
150
|
### Event Bus
|
|
149
151
|
|
|
@@ -197,7 +199,7 @@ Use decisions sparingly for plan-level constraints. Durable project facts belong
|
|
|
197
199
|
| Online | `ufoo online server|token|room|channel|connect|send|inbox` |
|
|
198
200
|
| History | `ufoo history build|show|prompt` |
|
|
199
201
|
| Skills | `ufoo skills list|install` |
|
|
200
|
-
| Chat
|
|
202
|
+
| Chat commands | `/skills`, `/settings`, `/settings agent`, `/settings router`, `/settings ucode` |
|
|
201
203
|
|
|
202
204
|
### Groups
|
|
203
205
|
|
|
@@ -246,8 +248,12 @@ Use the low-level queue runtime:
|
|
|
246
248
|
ucode-core submit --tool read --args-json '{"path":"README.md"}' --json
|
|
247
249
|
ucode-core run-once --json
|
|
248
250
|
ucode-core list --json
|
|
251
|
+
ucode-core skills list --json
|
|
252
|
+
ucode-core skills show <name>
|
|
249
253
|
```
|
|
250
254
|
|
|
255
|
+
`ucode-core skills list` discovers ufoo/ucode built-in and local `SKILL.md` preset workflow capabilities for selection. It lists metadata only; full skill bodies are loaded by ucode only when the user explicitly references a skill such as `$demo` or a direct `SKILL.md` link.
|
|
256
|
+
|
|
251
257
|
## Configuration
|
|
252
258
|
|
|
253
259
|
Project configuration is stored in `.ufoo/config.json`. `ucode` provider credentials are stored globally in `~/.ufoo/config.json` and merged into project config at load time.
|
package/README.zh-CN.md
CHANGED
|
@@ -133,7 +133,9 @@ ufoo resume <ucode|uclaude|ucodex|nickname>
|
|
|
133
133
|
ufoo recover list
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
常见 chat 命令包括 `/status`、`/bus list`、`/bus status`、`/settings`、`/project list`、`/project switch <index|path>`、`/open <path>`、`/resume list`、`/group status` 和 `@nickname <message>`。
|
|
136
|
+
常见 chat 命令包括 `/status`、`/bus list`、`/bus status`、`/settings`、`/project list`、`/project switch <index|path>`、`/open <path>`、`/resume list`、`/group status`、`/skills` 和 `@nickname <message>`。
|
|
137
|
+
|
|
138
|
+
在 `ufoo chat` 中,`/skills` 用于列出 ufoo 内置的可用 skills 和预设工作流能力,方便用户发现和选择。它本身不等于执行任务,也不表示某个 agent 的私有能力列表。
|
|
137
139
|
|
|
138
140
|
### 事件总线
|
|
139
141
|
|
|
@@ -211,8 +213,12 @@ ufoo ucode build
|
|
|
211
213
|
ucode-core submit --tool read --args-json '{"path":"README.md"}' --json
|
|
212
214
|
ucode-core run-once --json
|
|
213
215
|
ucode-core list --json
|
|
216
|
+
ucode-core skills list --json
|
|
217
|
+
ucode-core skills show <name>
|
|
214
218
|
```
|
|
215
219
|
|
|
220
|
+
`ucode-core skills list` 用于发现 ufoo/ucode 内置和本地 `SKILL.md` 预设工作流能力,输出的是供选择的元数据;完整 skill 内容只会在用户显式引用 `$demo` 或直接链接某个 `SKILL.md` 时由 ucode 加载。
|
|
221
|
+
|
|
216
222
|
## 命令参考
|
|
217
223
|
|
|
218
224
|
| 范围 | 命令 |
|
|
@@ -228,7 +234,7 @@ ucode-core list --json
|
|
|
228
234
|
| Online | `ufoo online server|token|room|channel|connect|send|inbox` |
|
|
229
235
|
| History | `ufoo history build|show|prompt` |
|
|
230
236
|
| Skills | `ufoo skills list|install` |
|
|
231
|
-
| Chat
|
|
237
|
+
| Chat 命令 | `/skills`, `/settings`, `/settings agent`, `/settings router`, `/settings ucode` |
|
|
232
238
|
|
|
233
239
|
## 配置
|
|
234
240
|
|
package/package.json
CHANGED
|
@@ -42,6 +42,7 @@ function createAgentViewController(options = {}) {
|
|
|
42
42
|
sendBusMessage = () => {},
|
|
43
43
|
sendResize = () => {},
|
|
44
44
|
requestScreenSnapshot = () => {},
|
|
45
|
+
sendBusWatch = () => {},
|
|
45
46
|
} = options;
|
|
46
47
|
|
|
47
48
|
if (!screen || typeof screen.render !== "function") {
|
|
@@ -69,10 +70,14 @@ function createAgentViewController(options = {}) {
|
|
|
69
70
|
};
|
|
70
71
|
|
|
71
72
|
function getRows() {
|
|
73
|
+
if (Number.isFinite(screen.height) && screen.height > 0) return screen.height;
|
|
74
|
+
if (Number.isFinite(screen.rows) && screen.rows > 0) return screen.rows;
|
|
72
75
|
return processStdout.rows || 24;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
function getCols() {
|
|
79
|
+
if (Number.isFinite(screen.width) && screen.width > 0) return screen.width;
|
|
80
|
+
if (Number.isFinite(screen.cols) && screen.cols > 0) return screen.cols;
|
|
76
81
|
return processStdout.columns || 80;
|
|
77
82
|
}
|
|
78
83
|
|
|
@@ -257,11 +262,14 @@ function createAgentViewController(options = {}) {
|
|
|
257
262
|
function forceScreenRepaint() {
|
|
258
263
|
if (typeof screen.realloc === "function") {
|
|
259
264
|
screen.realloc();
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
if (typeof screen.alloc === "function") {
|
|
265
|
+
} else if (typeof screen.alloc === "function") {
|
|
263
266
|
screen.alloc(true);
|
|
264
267
|
}
|
|
268
|
+
try {
|
|
269
|
+
originalRender();
|
|
270
|
+
} catch {
|
|
271
|
+
// Ignore repaint failures while restoring from raw agent view.
|
|
272
|
+
}
|
|
265
273
|
}
|
|
266
274
|
|
|
267
275
|
function compactProjectPath(projectRoot = "") {
|
|
@@ -297,13 +305,15 @@ function createAgentViewController(options = {}) {
|
|
|
297
305
|
const projectPath = compactProjectPath(getProjectRoot());
|
|
298
306
|
const product = "ClaudeCode";
|
|
299
307
|
const detail = label ? `${label} · managed headless` : "managed headless";
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
|
|
308
|
+
const iconWidth = 9;
|
|
309
|
+
const iconLine = (icon = "", text = "") => {
|
|
310
|
+
const pad = " ".repeat(Math.max(0, iconWidth - displayWidth(icon)));
|
|
311
|
+
return `${CLAUDE_ORANGE}${icon}${ANSI_RESET}${pad}${text}`;
|
|
312
|
+
};
|
|
303
313
|
const lines = [
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
314
|
+
iconLine("▐▛███▜▌", `${product}v${packageVersion}`),
|
|
315
|
+
iconLine("▝▜█████▛▘", detail),
|
|
316
|
+
iconLine(" ▘▘▝▝", projectPath),
|
|
307
317
|
"",
|
|
308
318
|
];
|
|
309
319
|
if (width < 44) return lines;
|
|
@@ -464,6 +474,7 @@ function createAgentViewController(options = {}) {
|
|
|
464
474
|
function enterAgentView(agentId, options = {}) {
|
|
465
475
|
if (currentView === "agent" && viewingAgent === agentId) return;
|
|
466
476
|
if (currentView === "agent") {
|
|
477
|
+
if (agentViewUsesBus && viewingAgent) sendBusWatch(viewingAgent, false);
|
|
467
478
|
disconnectAgentOutput();
|
|
468
479
|
disconnectAgentInput();
|
|
469
480
|
}
|
|
@@ -488,6 +499,7 @@ function createAgentViewController(options = {}) {
|
|
|
488
499
|
agentInputSuppressUntil = now() + 300;
|
|
489
500
|
agentViewUsesBus = Boolean(options.useBus);
|
|
490
501
|
if (agentViewUsesBus) {
|
|
502
|
+
sendBusWatch(agentId, true);
|
|
491
503
|
resetBusView(agentId);
|
|
492
504
|
renderBusView();
|
|
493
505
|
} else {
|
|
@@ -511,6 +523,7 @@ function createAgentViewController(options = {}) {
|
|
|
511
523
|
|
|
512
524
|
disconnectAgentOutput();
|
|
513
525
|
disconnectAgentInput();
|
|
526
|
+
if (agentViewUsesBus && viewingAgent) sendBusWatch(viewingAgent, false);
|
|
514
527
|
agentViewUsesBus = false;
|
|
515
528
|
agentOutputSuppressed = false;
|
|
516
529
|
agentBarVisible = false;
|
|
@@ -536,11 +549,11 @@ function createAgentViewController(options = {}) {
|
|
|
536
549
|
setDashboardView("agents");
|
|
537
550
|
setSelectedAgentIndex(-1);
|
|
538
551
|
setScreenGrabKeys(false);
|
|
539
|
-
forceScreenRepaint();
|
|
540
552
|
clearTargetAgent();
|
|
541
553
|
renderDashboard();
|
|
542
554
|
focusInput();
|
|
543
555
|
resizeInput();
|
|
556
|
+
forceScreenRepaint();
|
|
544
557
|
try {
|
|
545
558
|
if (screen.program && typeof screen.program.showCursor === "function") {
|
|
546
559
|
screen.program.showCursor();
|
|
@@ -448,12 +448,12 @@ function createCommandExecutor(options = {}) {
|
|
|
448
448
|
try {
|
|
449
449
|
const skills = createSkills(projectRoot);
|
|
450
450
|
|
|
451
|
-
if (subcommand === "list") {
|
|
451
|
+
if (!subcommand || subcommand === "list") {
|
|
452
452
|
const skillList = skills.list();
|
|
453
453
|
if (skillList.length === 0) {
|
|
454
|
-
logMessage("system", "{white-fg}No skills found{/white-fg}");
|
|
454
|
+
logMessage("system", "{white-fg}No built-in ufoo skills found{/white-fg}");
|
|
455
455
|
} else {
|
|
456
|
-
logMessage("system", `{cyan-fg}Available skills:{/cyan-fg} ${skillList.length}`);
|
|
456
|
+
logMessage("system", `{cyan-fg}Available built-in ufoo skills:{/cyan-fg} ${skillList.length}`);
|
|
457
457
|
for (const skill of skillList) {
|
|
458
458
|
logMessage("system", ` • ${skill}`);
|
|
459
459
|
}
|
package/src/chat/commands.js
CHANGED
|
@@ -129,10 +129,10 @@ const COMMAND_TREE = {
|
|
|
129
129
|
},
|
|
130
130
|
},
|
|
131
131
|
"/skills": {
|
|
132
|
-
desc: "
|
|
132
|
+
desc: "List ufoo built-in skills and preset workflows",
|
|
133
133
|
children: {
|
|
134
|
-
install: { desc: "Install skills (use: all or name)" },
|
|
135
|
-
list: { desc: "List
|
|
134
|
+
install: { desc: "Install built-in skills (use: all or name)" },
|
|
135
|
+
list: { desc: "List built-in skills for discovery" },
|
|
136
136
|
},
|
|
137
137
|
},
|
|
138
138
|
"/status": { desc: "Status display" },
|
|
@@ -19,6 +19,8 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
19
19
|
getCurrentView = () => "main",
|
|
20
20
|
isAgentViewUsesBus = () => false,
|
|
21
21
|
getViewingAgent = () => "",
|
|
22
|
+
isAgentEventForViewingAgent = (data, viewingAgent, publisher) =>
|
|
23
|
+
Boolean(viewingAgent && (publisher === viewingAgent || data?.target === viewingAgent)),
|
|
22
24
|
writeToAgentTerm = () => {},
|
|
23
25
|
consumePendingDelivery = () => false,
|
|
24
26
|
getPendingState = () => null,
|
|
@@ -352,6 +354,19 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
352
354
|
function handleBusMessage(msg) {
|
|
353
355
|
const data = msg.data || {};
|
|
354
356
|
if (data.event === "activity_state_changed") {
|
|
357
|
+
const publisher = data.publisher && data.publisher !== "unknown"
|
|
358
|
+
? data.publisher
|
|
359
|
+
: (data.subscriber || "bus");
|
|
360
|
+
const viewingAgent = getViewingAgent();
|
|
361
|
+
if (
|
|
362
|
+
getCurrentView() === "agent" &&
|
|
363
|
+
isAgentViewUsesBus() &&
|
|
364
|
+
viewingAgent &&
|
|
365
|
+
isAgentEventForViewingAgent(data, viewingAgent, publisher)
|
|
366
|
+
) {
|
|
367
|
+
const state = data.state || (data.data && data.data.state) || "";
|
|
368
|
+
if (state) writeToAgentTerm(`[state] ${state}\r\n`);
|
|
369
|
+
}
|
|
355
370
|
requestStatus();
|
|
356
371
|
return true;
|
|
357
372
|
}
|
|
@@ -375,11 +390,12 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
375
390
|
if (data.silent && !streamPayload) return true;
|
|
376
391
|
if (!displayMessage && !streamPayload) return true;
|
|
377
392
|
|
|
393
|
+
const viewingAgent = getViewingAgent();
|
|
378
394
|
const isAgentViewTarget =
|
|
379
395
|
getCurrentView() === "agent" &&
|
|
380
396
|
isAgentViewUsesBus() &&
|
|
381
|
-
|
|
382
|
-
|
|
397
|
+
viewingAgent &&
|
|
398
|
+
isAgentEventForViewingAgent(data, viewingAgent, publisher);
|
|
383
399
|
|
|
384
400
|
const displayName = resolveAgentDisplayName(publisher);
|
|
385
401
|
|
package/src/chat/index.js
CHANGED
|
@@ -1581,6 +1581,14 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1581
1581
|
source: "chat-internal-agent-view",
|
|
1582
1582
|
});
|
|
1583
1583
|
},
|
|
1584
|
+
sendBusWatch: (agentId, enabled) => {
|
|
1585
|
+
if (!agentId) return;
|
|
1586
|
+
send({
|
|
1587
|
+
type: IPC_REQUEST_TYPES.BUS_WATCH,
|
|
1588
|
+
agent_id: agentId,
|
|
1589
|
+
enabled: enabled !== false,
|
|
1590
|
+
});
|
|
1591
|
+
},
|
|
1584
1592
|
sendResize: (cols, rows) => {
|
|
1585
1593
|
sendResizeWithCapabilities(cols, rows);
|
|
1586
1594
|
},
|
|
@@ -1612,6 +1620,21 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1612
1620
|
getCurrentView: () => getCurrentView(),
|
|
1613
1621
|
isAgentViewUsesBus: () => isAgentViewUsesBus(),
|
|
1614
1622
|
getViewingAgent: () => getViewingAgent(),
|
|
1623
|
+
isAgentEventForViewingAgent: (data, viewingAgent, publisher) => {
|
|
1624
|
+
if (!viewingAgent) return false;
|
|
1625
|
+
const label = getAgentLabel(viewingAgent);
|
|
1626
|
+
const candidates = [
|
|
1627
|
+
publisher,
|
|
1628
|
+
data && data.publisher,
|
|
1629
|
+
data && data.target,
|
|
1630
|
+
data && data.subscriber,
|
|
1631
|
+
].filter(Boolean);
|
|
1632
|
+
return candidates.some((value) => (
|
|
1633
|
+
value === viewingAgent ||
|
|
1634
|
+
value === label ||
|
|
1635
|
+
resolveAgentId(value) === viewingAgent
|
|
1636
|
+
));
|
|
1637
|
+
},
|
|
1615
1638
|
writeToAgentTerm,
|
|
1616
1639
|
consumePendingDelivery,
|
|
1617
1640
|
getPendingState,
|
package/src/code/agent.js
CHANGED
|
@@ -25,6 +25,12 @@ const {
|
|
|
25
25
|
loadSessionSnapshot,
|
|
26
26
|
} = require("./sessionStore");
|
|
27
27
|
const { buildPromptContext } = require("./prompts");
|
|
28
|
+
const {
|
|
29
|
+
buildSkillInjections,
|
|
30
|
+
formatSkillsList,
|
|
31
|
+
listUcodeSkills,
|
|
32
|
+
showSkill,
|
|
33
|
+
} = require("./skills");
|
|
28
34
|
|
|
29
35
|
function printPrompt() {
|
|
30
36
|
process.stdout.write("> ");
|
|
@@ -243,6 +249,61 @@ function createToolLogCollector(logs = [], onToolLog = null) {
|
|
|
243
249
|
};
|
|
244
250
|
}
|
|
245
251
|
|
|
252
|
+
function pushSkillWarning(logs = [], onToolLog = null, warning = "") {
|
|
253
|
+
const text = String(warning || "").trim();
|
|
254
|
+
if (!text) return null;
|
|
255
|
+
const entry = {
|
|
256
|
+
type: "skills",
|
|
257
|
+
phase: "warning",
|
|
258
|
+
message: text,
|
|
259
|
+
error: text,
|
|
260
|
+
};
|
|
261
|
+
if (Array.isArray(logs)) logs.push(entry);
|
|
262
|
+
if (typeof onToolLog === "function") {
|
|
263
|
+
try {
|
|
264
|
+
onToolLog(entry);
|
|
265
|
+
} catch {
|
|
266
|
+
// ignore callback failures
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return entry;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function stripSkillBlocksFromText(value = "") {
|
|
273
|
+
return String(value || "")
|
|
274
|
+
.replace(/<skill>\s*[\s\S]*?<\/skill>\s*/g, "")
|
|
275
|
+
.trim();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function stripSkillBlocksFromMessages(messages = []) {
|
|
279
|
+
if (!Array.isArray(messages)) return [];
|
|
280
|
+
return messages.map((message) => {
|
|
281
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) return message;
|
|
282
|
+
if (typeof message.content === "string") {
|
|
283
|
+
return {
|
|
284
|
+
...message,
|
|
285
|
+
content: stripSkillBlocksFromText(message.content),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (Array.isArray(message.content)) {
|
|
289
|
+
return {
|
|
290
|
+
...message,
|
|
291
|
+
content: message.content.map((item) => {
|
|
292
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
293
|
+
if (typeof item.text === "string") {
|
|
294
|
+
return {
|
|
295
|
+
...item,
|
|
296
|
+
text: stripSkillBlocksFromText(item.text),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return item;
|
|
300
|
+
}),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return message;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
246
307
|
function isProjectAnalysisTask(task = "") {
|
|
247
308
|
const text = String(task || "").trim().toLowerCase();
|
|
248
309
|
if (!text) return false;
|
|
@@ -398,6 +459,16 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
398
459
|
const taskPrompt = analysisTask
|
|
399
460
|
? `${taskText}\n\nAnalysis requirements:\n- Inspect repository evidence before concluding.\n- Cite concrete file observations.\n- Keep findings concise and actionable.`
|
|
400
461
|
: taskText;
|
|
462
|
+
const skillInjections = buildSkillInjections({
|
|
463
|
+
prompt: taskPrompt,
|
|
464
|
+
workspaceRoot,
|
|
465
|
+
});
|
|
466
|
+
for (const warning of skillInjections.warnings || []) {
|
|
467
|
+
pushSkillWarning(logs, onToolLog, warning);
|
|
468
|
+
}
|
|
469
|
+
const effectiveTaskPrompt = Array.isArray(skillInjections.blocks) && skillInjections.blocks.length > 0
|
|
470
|
+
? `${skillInjections.blocks.join("\n\n")}\n\n${taskPrompt}`
|
|
471
|
+
: taskPrompt;
|
|
401
472
|
const systemContext = [String(state.context || "").trim(), preflightContext]
|
|
402
473
|
.filter(Boolean)
|
|
403
474
|
.join("\n\n");
|
|
@@ -422,7 +493,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
422
493
|
workspaceRoot,
|
|
423
494
|
provider,
|
|
424
495
|
model,
|
|
425
|
-
prompt:
|
|
496
|
+
prompt: effectiveTaskPrompt,
|
|
426
497
|
systemPrompt: systemContext,
|
|
427
498
|
messages: Array.isArray(state.nlMessages) ? state.nlMessages : [],
|
|
428
499
|
sessionId: String(sessionIdValue || ""),
|
|
@@ -440,7 +511,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
440
511
|
// Use decomposed runner for bug fix tasks
|
|
441
512
|
if (useDecomposition) {
|
|
442
513
|
const decomposedResult = await runDecomposedTask({
|
|
443
|
-
task:
|
|
514
|
+
task: effectiveTaskPrompt,
|
|
444
515
|
state,
|
|
445
516
|
onProgress: options.onProgress,
|
|
446
517
|
onToolEvent: pushToolLog,
|
|
@@ -497,7 +568,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
497
568
|
state.sessionId = cliRes.sessionId.trim();
|
|
498
569
|
}
|
|
499
570
|
if (cliRes && Array.isArray(cliRes.messages)) {
|
|
500
|
-
state.nlMessages = cliRes.messages;
|
|
571
|
+
state.nlMessages = stripSkillBlocksFromMessages(cliRes.messages);
|
|
501
572
|
}
|
|
502
573
|
const normalized = String(cliRes.output || "").trim();
|
|
503
574
|
const summary = extractJsonSummary(normalized);
|
|
@@ -1235,6 +1306,8 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
|
1235
1306
|
" help",
|
|
1236
1307
|
" exit|quit",
|
|
1237
1308
|
" ubus|/ubus",
|
|
1309
|
+
" skills [list]",
|
|
1310
|
+
" skills show <name>",
|
|
1238
1311
|
" bg|/bg <task>",
|
|
1239
1312
|
" resume <session-id>",
|
|
1240
1313
|
" tool <read|write|edit|bash> <args-json>",
|
|
@@ -1254,6 +1327,45 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
|
1254
1327
|
kind: "ubus",
|
|
1255
1328
|
};
|
|
1256
1329
|
}
|
|
1330
|
+
const skillsMatch = text.match(/^(?:\/skills|skills)(?:\s+(.*))?$/i);
|
|
1331
|
+
if (skillsMatch) {
|
|
1332
|
+
const args = String(skillsMatch[1] || "").trim().split(/\s+/).filter(Boolean);
|
|
1333
|
+
const action = String(args[0] || "list").toLowerCase();
|
|
1334
|
+
if (action === "list" || action === "ls") {
|
|
1335
|
+
const outcome = listUcodeSkills({ workspaceRoot });
|
|
1336
|
+
return {
|
|
1337
|
+
kind: "skills",
|
|
1338
|
+
output: formatSkillsList(outcome),
|
|
1339
|
+
skills: outcome.skills,
|
|
1340
|
+
errors: outcome.errors,
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
if (action === "show") {
|
|
1344
|
+
const name = String(args[1] || "").trim();
|
|
1345
|
+
if (!name) {
|
|
1346
|
+
return {
|
|
1347
|
+
kind: "error",
|
|
1348
|
+
output: "usage: skills show <name>",
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
const result = showSkill({ name, workspaceRoot });
|
|
1352
|
+
if (!result.ok) {
|
|
1353
|
+
return {
|
|
1354
|
+
kind: "error",
|
|
1355
|
+
output: result.error,
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
return {
|
|
1359
|
+
kind: "skills",
|
|
1360
|
+
output: result.output,
|
|
1361
|
+
skill: result.skill,
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
kind: "error",
|
|
1366
|
+
output: "usage: skills [list] | skills show <name>",
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1257
1369
|
if (text === "bg" || text === "/bg") {
|
|
1258
1370
|
return {
|
|
1259
1371
|
kind: "error",
|
|
@@ -1498,7 +1610,7 @@ async function runUcodeCoreAgent({
|
|
|
1498
1610
|
if (result.kind === "probe") {
|
|
1499
1611
|
return;
|
|
1500
1612
|
}
|
|
1501
|
-
if (result.kind === "help" || result.kind === "tool" || result.kind === "error") {
|
|
1613
|
+
if (result.kind === "help" || result.kind === "tool" || result.kind === "skills" || result.kind === "error") {
|
|
1502
1614
|
stdout.write(`${result.output}\n`);
|
|
1503
1615
|
}
|
|
1504
1616
|
if (result.kind === "ubus") {
|
|
@@ -1701,6 +1813,8 @@ module.exports = {
|
|
|
1701
1813
|
normalizeToolLogEvent,
|
|
1702
1814
|
isProjectAnalysisTask,
|
|
1703
1815
|
buildNlFallbackSummary,
|
|
1816
|
+
buildNlContext,
|
|
1817
|
+
stripSkillBlocksFromMessages,
|
|
1704
1818
|
resolvePlannerProvider,
|
|
1705
1819
|
extractJsonSummary,
|
|
1706
1820
|
enrichNativeError,
|
package/src/code/cli.js
CHANGED
|
@@ -4,6 +4,11 @@ const {
|
|
|
4
4
|
listResults,
|
|
5
5
|
} = require("./runtime");
|
|
6
6
|
const { runUcodeCoreAgent } = require("./agent");
|
|
7
|
+
const {
|
|
8
|
+
formatSkillsList,
|
|
9
|
+
listUcodeSkills,
|
|
10
|
+
showSkill,
|
|
11
|
+
} = require("./skills");
|
|
7
12
|
|
|
8
13
|
function parseArgs(argv = []) {
|
|
9
14
|
const args = Array.isArray(argv) ? argv.slice() : [];
|
|
@@ -16,11 +21,21 @@ function parseArgs(argv = []) {
|
|
|
16
21
|
max: 1,
|
|
17
22
|
num: 20,
|
|
18
23
|
taskId: "",
|
|
24
|
+
skillsAction: "",
|
|
25
|
+
skillsName: "",
|
|
19
26
|
};
|
|
20
27
|
|
|
21
28
|
for (let i = 1; i < args.length; i += 1) {
|
|
22
29
|
const item = String(args[i] || "").trim();
|
|
23
30
|
if (!item) continue;
|
|
31
|
+
if (out.command === "skills" && !item.startsWith("-")) {
|
|
32
|
+
if (!out.skillsAction) {
|
|
33
|
+
out.skillsAction = item.toLowerCase();
|
|
34
|
+
} else if (!out.skillsName) {
|
|
35
|
+
out.skillsName = item;
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
24
39
|
if (item === "--json") {
|
|
25
40
|
out.json = true;
|
|
26
41
|
continue;
|
|
@@ -69,6 +84,8 @@ function usage() {
|
|
|
69
84
|
" submit --tool <read|write|edit|bash> --args-json <json> [--workspace <path>] [--task-id <id>]",
|
|
70
85
|
" run-once [--max <n>] [--workspace <path>]",
|
|
71
86
|
" list [--num <n>]",
|
|
87
|
+
" skills list [--workspace <path>]",
|
|
88
|
+
" skills show <name> [--workspace <path>]",
|
|
72
89
|
"",
|
|
73
90
|
"Flags:",
|
|
74
91
|
" --json Output JSON",
|
|
@@ -147,6 +164,36 @@ async function runUcodeCoreCli({
|
|
|
147
164
|
return { exitCode: 0, output: `${lines.join("\n")}${lines.length ? "\n" : ""}` };
|
|
148
165
|
}
|
|
149
166
|
|
|
167
|
+
if (cmd === "skills") {
|
|
168
|
+
const action = options.skillsAction || "list";
|
|
169
|
+
const workspaceRoot = options.workspace || projectRoot;
|
|
170
|
+
if (action === "list" || action === "ls") {
|
|
171
|
+
const outcome = listUcodeSkills({ workspaceRoot });
|
|
172
|
+
if (options.json) {
|
|
173
|
+
return { exitCode: 0, output: `${JSON.stringify({ ok: true, ...outcome })}\n` };
|
|
174
|
+
}
|
|
175
|
+
return { exitCode: 0, output: `${formatSkillsList(outcome)}\n` };
|
|
176
|
+
}
|
|
177
|
+
if (action === "show") {
|
|
178
|
+
if (!options.skillsName) {
|
|
179
|
+
return { exitCode: 1, output: "skills show requires <name>\n" };
|
|
180
|
+
}
|
|
181
|
+
const result = showSkill({
|
|
182
|
+
name: options.skillsName,
|
|
183
|
+
workspaceRoot,
|
|
184
|
+
asJson: options.json,
|
|
185
|
+
});
|
|
186
|
+
if (!result.ok) {
|
|
187
|
+
return { exitCode: 1, output: `${options.json ? JSON.stringify({ ok: false, error: result.error }) : result.error}\n` };
|
|
188
|
+
}
|
|
189
|
+
if (options.json) {
|
|
190
|
+
return { exitCode: 0, output: `${JSON.stringify(result)}\n` };
|
|
191
|
+
}
|
|
192
|
+
return { exitCode: 0, output: `${result.output}\n` };
|
|
193
|
+
}
|
|
194
|
+
return { exitCode: 1, output: "unknown skills command: use list or show\n" };
|
|
195
|
+
}
|
|
196
|
+
|
|
150
197
|
return { exitCode: 1, output: `unknown command: ${cmd}\n` };
|
|
151
198
|
}
|
|
152
199
|
|
|
@@ -8,8 +8,13 @@ const { getSafetySection } = require("./safety");
|
|
|
8
8
|
const { getOutputEfficiencySection } = require("./efficiency");
|
|
9
9
|
const { getUfooIntegrationSection } = require("./ufoo");
|
|
10
10
|
const { getEnvironmentSection } = require("./environment");
|
|
11
|
+
const {
|
|
12
|
+
listUcodeSkills,
|
|
13
|
+
renderSkillsSection,
|
|
14
|
+
} = require("../skills");
|
|
11
15
|
const {
|
|
12
16
|
systemPromptSection,
|
|
17
|
+
uncachedSection,
|
|
13
18
|
resolveSections,
|
|
14
19
|
clearSectionCache,
|
|
15
20
|
} = require("./sections");
|
|
@@ -68,6 +73,10 @@ function getSystemPrompt({
|
|
|
68
73
|
systemPromptSection("environment", () =>
|
|
69
74
|
getEnvironmentSection({ workspaceRoot, model, provider }),
|
|
70
75
|
),
|
|
76
|
+
uncachedSection("skills", () => {
|
|
77
|
+
const outcome = listUcodeSkills({ workspaceRoot: workspaceRoot || process.cwd() });
|
|
78
|
+
return renderSkillsSection(outcome.skills);
|
|
79
|
+
}),
|
|
71
80
|
];
|
|
72
81
|
const dynamicSections = resolveSections(dynamicSectionDefs);
|
|
73
82
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const {
|
|
2
|
+
listUcodeSkills,
|
|
3
|
+
findSkillsByName,
|
|
4
|
+
} = require("./loader");
|
|
5
|
+
const {
|
|
6
|
+
renderSkillsSection,
|
|
7
|
+
formatSkillsList,
|
|
8
|
+
} = require("./render");
|
|
9
|
+
const {
|
|
10
|
+
buildSkillInjections,
|
|
11
|
+
} = require("./injection");
|
|
12
|
+
|
|
13
|
+
function showSkill({ name = "", workspaceRoot = process.cwd(), asJson = false } = {}) {
|
|
14
|
+
const outcome = listUcodeSkills({ workspaceRoot });
|
|
15
|
+
const matches = findSkillsByName(outcome.skills, name);
|
|
16
|
+
if (matches.length === 0) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
error: `skill not found: ${name}`,
|
|
20
|
+
outcome,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (matches.length > 1) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
error: `skill is ambiguous: ${name}`,
|
|
27
|
+
outcome,
|
|
28
|
+
matches,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const skill = matches[0];
|
|
32
|
+
let content = "";
|
|
33
|
+
try {
|
|
34
|
+
content = require("fs").readFileSync(skill.path, "utf8");
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
error: err && err.message ? err.message : "failed to read skill",
|
|
39
|
+
outcome,
|
|
40
|
+
skill,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (asJson) {
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
skill,
|
|
47
|
+
content,
|
|
48
|
+
errors: outcome.errors,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
output: [
|
|
54
|
+
`# ${skill.name}`,
|
|
55
|
+
"",
|
|
56
|
+
`Description: ${skill.description}`,
|
|
57
|
+
`Path: ${skill.path}`,
|
|
58
|
+
"",
|
|
59
|
+
content.trim(),
|
|
60
|
+
].join("\n"),
|
|
61
|
+
skill,
|
|
62
|
+
content,
|
|
63
|
+
errors: outcome.errors,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
listUcodeSkills,
|
|
69
|
+
findSkillsByName,
|
|
70
|
+
renderSkillsSection,
|
|
71
|
+
formatSkillsList,
|
|
72
|
+
buildSkillInjections,
|
|
73
|
+
showSkill,
|
|
74
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const {
|
|
4
|
+
canonicalPath,
|
|
5
|
+
findSkillsByName,
|
|
6
|
+
listUcodeSkills,
|
|
7
|
+
} = require("./loader");
|
|
8
|
+
|
|
9
|
+
function markdownSkillLinks(prompt = "") {
|
|
10
|
+
const text = String(prompt || "");
|
|
11
|
+
const links = [];
|
|
12
|
+
const re = /\[([^\]]+)]\(([^)]+)\)/g;
|
|
13
|
+
let match;
|
|
14
|
+
while ((match = re.exec(text))) {
|
|
15
|
+
const label = String(match[1] || "").trim();
|
|
16
|
+
const target = String(match[2] || "").trim();
|
|
17
|
+
if (!target) continue;
|
|
18
|
+
if (target.startsWith("skill://") || /(?:^|[/\\])SKILL\.md(?:[#?].*)?$/i.test(target)) {
|
|
19
|
+
links.push({ label, target });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return links;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mentionedSkillNames(prompt = "") {
|
|
26
|
+
const text = String(prompt || "");
|
|
27
|
+
const names = new Set();
|
|
28
|
+
const re = /(^|[^\w-])\$([A-Za-z0-9][A-Za-z0-9_-]{0,63})\b/g;
|
|
29
|
+
let match;
|
|
30
|
+
while ((match = re.exec(text))) {
|
|
31
|
+
names.add(String(match[2] || "").trim());
|
|
32
|
+
}
|
|
33
|
+
return Array.from(names).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveSkillLinkTarget(target = "", workspaceRoot = process.cwd()) {
|
|
37
|
+
const raw = String(target || "").trim().replace(/[#?].*$/, "");
|
|
38
|
+
if (!raw) return "";
|
|
39
|
+
let value = raw;
|
|
40
|
+
if (value.startsWith("skill://")) {
|
|
41
|
+
value = value.slice("skill://".length);
|
|
42
|
+
try {
|
|
43
|
+
value = decodeURIComponent(value);
|
|
44
|
+
} catch {
|
|
45
|
+
// keep raw value
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!value) return "";
|
|
49
|
+
if (path.isAbsolute(value)) return canonicalPath(value);
|
|
50
|
+
return canonicalPath(path.resolve(workspaceRoot || process.cwd(), value));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function findSkillByPath(skills = [], targetPath = "") {
|
|
54
|
+
const target = canonicalPath(targetPath);
|
|
55
|
+
return (Array.isArray(skills) ? skills : []).find((skill) => canonicalPath(skill.path) === target) || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readSkillBlock(skill) {
|
|
59
|
+
const content = fs.readFileSync(skill.path, "utf8");
|
|
60
|
+
return `<skill>\n<name>${skill.name}</name>\n<path>${String(skill.path).replace(/\\/g, "/")}</path>\n${content}\n</skill>`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildSkillInjections({
|
|
64
|
+
prompt = "",
|
|
65
|
+
workspaceRoot = process.cwd(),
|
|
66
|
+
skillsOutcome = null,
|
|
67
|
+
loadSkills = listUcodeSkills,
|
|
68
|
+
} = {}) {
|
|
69
|
+
const outcome = skillsOutcome || loadSkills({ workspaceRoot });
|
|
70
|
+
const skills = Array.isArray(outcome.skills) ? outcome.skills : [];
|
|
71
|
+
const warnings = Array.isArray(outcome.errors)
|
|
72
|
+
? outcome.errors.map((err) => `failed to load skill ${err.path}: ${err.message}`)
|
|
73
|
+
: [];
|
|
74
|
+
const selected = new Map();
|
|
75
|
+
|
|
76
|
+
for (const name of mentionedSkillNames(prompt)) {
|
|
77
|
+
const matches = findSkillsByName(skills, name);
|
|
78
|
+
if (matches.length === 1) {
|
|
79
|
+
selected.set(canonicalPath(matches[0].path), matches[0]);
|
|
80
|
+
} else if (matches.length > 1) {
|
|
81
|
+
warnings.push(`skill $${name} is ambiguous; link to a specific SKILL.md path`);
|
|
82
|
+
} else {
|
|
83
|
+
warnings.push(`skill $${name} was not found`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const link of markdownSkillLinks(prompt)) {
|
|
88
|
+
const targetPath = resolveSkillLinkTarget(link.target, workspaceRoot);
|
|
89
|
+
const skill = findSkillByPath(skills, targetPath);
|
|
90
|
+
if (skill) {
|
|
91
|
+
selected.set(canonicalPath(skill.path), skill);
|
|
92
|
+
} else {
|
|
93
|
+
warnings.push(`skill link ${link.target} did not resolve to an enabled skill`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const blocks = [];
|
|
98
|
+
for (const skill of selected.values()) {
|
|
99
|
+
try {
|
|
100
|
+
blocks.push(readSkillBlock(skill));
|
|
101
|
+
} catch (err) {
|
|
102
|
+
warnings.push(`failed to read skill ${skill.path}: ${err && err.message ? err.message : "read failed"}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
blocks,
|
|
108
|
+
warnings,
|
|
109
|
+
skills,
|
|
110
|
+
errors: Array.isArray(outcome.errors) ? outcome.errors : [],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
mentionedSkillNames,
|
|
116
|
+
markdownSkillLinks,
|
|
117
|
+
resolveSkillLinkTarget,
|
|
118
|
+
findSkillByPath,
|
|
119
|
+
buildSkillInjections,
|
|
120
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const matter = require("gray-matter");
|
|
5
|
+
|
|
6
|
+
const SKILL_FILE = "SKILL.md";
|
|
7
|
+
const DEFAULT_MAX_DEPTH = 6;
|
|
8
|
+
|
|
9
|
+
function repoRootFromHere() {
|
|
10
|
+
return path.resolve(__dirname, "..", "..", "..");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function canonicalPath(filePath = "") {
|
|
14
|
+
const resolved = path.resolve(String(filePath || ""));
|
|
15
|
+
try {
|
|
16
|
+
return fs.realpathSync.native ? fs.realpathSync.native(resolved) : fs.realpathSync(resolved);
|
|
17
|
+
} catch {
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isDirectory(filePath = "") {
|
|
23
|
+
try {
|
|
24
|
+
return fs.statSync(filePath).isDirectory();
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isFile(filePath = "") {
|
|
31
|
+
try {
|
|
32
|
+
return fs.statSync(filePath).isFile();
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function homeDir(env = process.env) {
|
|
39
|
+
return String((env && env.HOME) || os.homedir() || "").trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function defaultSkillRoots({
|
|
43
|
+
workspaceRoot = process.cwd(),
|
|
44
|
+
env = process.env,
|
|
45
|
+
repoRoot = repoRootFromHere(),
|
|
46
|
+
} = {}) {
|
|
47
|
+
const workspace = path.resolve(String(workspaceRoot || process.cwd()));
|
|
48
|
+
const home = homeDir(env);
|
|
49
|
+
const codexHome = String((env && env.CODEX_HOME) || "").trim()
|
|
50
|
+
|| (home ? path.join(home, ".codex") : "");
|
|
51
|
+
const roots = [
|
|
52
|
+
{ path: path.join(workspace, ".agents", "skills"), scope: "repo", source: "workspace-agents" },
|
|
53
|
+
{ path: path.join(workspace, ".codex", "skills"), scope: "repo", source: "workspace-codex" },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
if (home) {
|
|
57
|
+
roots.push({ path: path.join(home, ".agents", "skills"), scope: "user", source: "user-agents" });
|
|
58
|
+
}
|
|
59
|
+
if (codexHome) {
|
|
60
|
+
roots.push({ path: path.join(codexHome, "skills"), scope: "user", source: "user-codex" });
|
|
61
|
+
roots.push({ path: path.join(codexHome, "skills", ".system"), scope: "system", source: "codex-system" });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const root = path.resolve(String(repoRoot || repoRootFromHere()));
|
|
65
|
+
roots.push({ path: path.join(root, "SKILLS"), scope: "builtin", source: "ufoo" });
|
|
66
|
+
const modulesDir = path.join(root, "modules");
|
|
67
|
+
try {
|
|
68
|
+
for (const entry of fs.readdirSync(modulesDir, { withFileTypes: true })) {
|
|
69
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
70
|
+
roots.push({
|
|
71
|
+
path: path.join(modulesDir, entry.name, "SKILLS"),
|
|
72
|
+
scope: "builtin",
|
|
73
|
+
source: `ufoo-module:${entry.name}`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Modules are optional in tests and local installs.
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
return roots
|
|
82
|
+
.map((rootInfo) => ({ ...rootInfo, path: path.resolve(rootInfo.path) }))
|
|
83
|
+
.filter((rootInfo) => {
|
|
84
|
+
if (seen.has(rootInfo.path)) return false;
|
|
85
|
+
seen.add(rootInfo.path);
|
|
86
|
+
return true;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseSkillFile(filePath, rootInfo = {}) {
|
|
91
|
+
const skillPath = canonicalPath(filePath);
|
|
92
|
+
const dir = path.dirname(skillPath);
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = matter(fs.readFileSync(skillPath, "utf8"));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
throw new Error(err && err.message ? err.message : "failed to read skill");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const data = parsed && parsed.data && typeof parsed.data === "object" ? parsed.data : {};
|
|
101
|
+
const name = String(data.name || "").trim();
|
|
102
|
+
const description = String(data.description || "").trim();
|
|
103
|
+
if (!name) throw new Error("missing required frontmatter field: name");
|
|
104
|
+
if (!description) throw new Error("missing required frontmatter field: description");
|
|
105
|
+
|
|
106
|
+
const metadata = data.metadata && typeof data.metadata === "object" ? data.metadata : {};
|
|
107
|
+
const shortDescription = String(
|
|
108
|
+
metadata["short-description"]
|
|
109
|
+
|| metadata.shortDescription
|
|
110
|
+
|| data["short-description"]
|
|
111
|
+
|| data.shortDescription
|
|
112
|
+
|| ""
|
|
113
|
+
).trim();
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
name,
|
|
117
|
+
description,
|
|
118
|
+
shortDescription,
|
|
119
|
+
path: skillPath,
|
|
120
|
+
dir,
|
|
121
|
+
scope: rootInfo.scope || "repo",
|
|
122
|
+
source: rootInfo.source || "",
|
|
123
|
+
enabled: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function discoverSkillsUnderRoot(rootInfo = {}, options = {}) {
|
|
128
|
+
const root = path.resolve(String(rootInfo.path || ""));
|
|
129
|
+
const maxDepth = Number.isFinite(options.maxDepth) ? options.maxDepth : DEFAULT_MAX_DEPTH;
|
|
130
|
+
const skills = [];
|
|
131
|
+
const errors = [];
|
|
132
|
+
if (!root || !isDirectory(root)) return { skills, errors };
|
|
133
|
+
|
|
134
|
+
const walk = (dir, depth) => {
|
|
135
|
+
if (depth > maxDepth) return;
|
|
136
|
+
const skillFile = path.join(dir, SKILL_FILE);
|
|
137
|
+
if (isFile(skillFile)) {
|
|
138
|
+
try {
|
|
139
|
+
skills.push(parseSkillFile(skillFile, rootInfo));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
errors.push({
|
|
142
|
+
path: canonicalPath(skillFile),
|
|
143
|
+
message: err && err.message ? err.message : "invalid skill",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let entries = [];
|
|
150
|
+
try {
|
|
151
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
152
|
+
} catch (err) {
|
|
153
|
+
errors.push({
|
|
154
|
+
path: canonicalPath(dir),
|
|
155
|
+
message: err && err.message ? err.message : "failed to read directory",
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (!entry.isDirectory()) continue;
|
|
162
|
+
if (entry.name.startsWith(".")) continue;
|
|
163
|
+
walk(path.join(dir, entry.name), depth + 1);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
walk(root, 0);
|
|
168
|
+
return { skills, errors };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function listUcodeSkills(options = {}) {
|
|
172
|
+
const roots = Array.isArray(options.roots)
|
|
173
|
+
? options.roots
|
|
174
|
+
: defaultSkillRoots(options);
|
|
175
|
+
const seenPaths = new Set();
|
|
176
|
+
const skills = [];
|
|
177
|
+
const errors = [];
|
|
178
|
+
|
|
179
|
+
for (const rootInfo of roots) {
|
|
180
|
+
const discovered = discoverSkillsUnderRoot(rootInfo, options);
|
|
181
|
+
for (const err of discovered.errors) errors.push(err);
|
|
182
|
+
for (const skill of discovered.skills) {
|
|
183
|
+
const key = canonicalPath(skill.path);
|
|
184
|
+
if (seenPaths.has(key)) continue;
|
|
185
|
+
seenPaths.add(key);
|
|
186
|
+
skills.push(skill);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const scopeRank = { repo: 0, user: 1, builtin: 2, system: 3, admin: 4 };
|
|
191
|
+
skills.sort((a, b) => {
|
|
192
|
+
const rankA = scopeRank[a.scope] == null ? 9 : scopeRank[a.scope];
|
|
193
|
+
const rankB = scopeRank[b.scope] == null ? 9 : scopeRank[b.scope];
|
|
194
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
195
|
+
if (a.name !== b.name) return a.name.localeCompare(b.name);
|
|
196
|
+
return a.path.localeCompare(b.path);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return { skills, errors };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function findSkillsByName(skills = [], name = "") {
|
|
203
|
+
const target = String(name || "").trim().toLowerCase();
|
|
204
|
+
if (!target) return [];
|
|
205
|
+
return (Array.isArray(skills) ? skills : [])
|
|
206
|
+
.filter((skill) => skill && skill.enabled !== false && String(skill.name || "").trim().toLowerCase() === target);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
SKILL_FILE,
|
|
211
|
+
DEFAULT_MAX_DEPTH,
|
|
212
|
+
canonicalPath,
|
|
213
|
+
defaultSkillRoots,
|
|
214
|
+
parseSkillFile,
|
|
215
|
+
discoverSkillsUnderRoot,
|
|
216
|
+
listUcodeSkills,
|
|
217
|
+
findSkillsByName,
|
|
218
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function renderSkillsSection(skills = []) {
|
|
2
|
+
const list = (Array.isArray(skills) ? skills : []).filter((skill) => skill && skill.enabled !== false);
|
|
3
|
+
if (list.length === 0) return "";
|
|
4
|
+
|
|
5
|
+
const lines = [];
|
|
6
|
+
lines.push("## Skills");
|
|
7
|
+
lines.push("ufoo/ucode skills are built-in or local preset workflow capabilities discovered from SKILL.md files. The list below is for discovery and selection; it is not a private capability list for one agent, and the full skill body is loaded only when a user explicitly requests a skill.");
|
|
8
|
+
lines.push("### Available skills");
|
|
9
|
+
|
|
10
|
+
for (const skill of list) {
|
|
11
|
+
const pathText = String(skill.path || "").replace(/\\/g, "/");
|
|
12
|
+
const desc = String(skill.description || skill.shortDescription || "").trim();
|
|
13
|
+
lines.push(`- ${skill.name}: ${desc} (file: ${pathText})`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
lines.push("### How to use skills");
|
|
17
|
+
lines.push("- If the user names a skill with `$SkillName` or links directly to a `SKILL.md`, use that skill for this turn.");
|
|
18
|
+
lines.push("- Do not assume a skill applies just because it exists; match the user request to the listed skill descriptions.");
|
|
19
|
+
lines.push("- When a skill is selected, read only the specific skill body and nearby referenced files needed for the task.");
|
|
20
|
+
lines.push("- If a skill is ambiguous, missing, or unreadable, say so briefly and continue with the best fallback.");
|
|
21
|
+
|
|
22
|
+
return lines.join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatSkillsList({ skills = [], errors = [] } = {}) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
const list = (Array.isArray(skills) ? skills : []).filter((skill) => skill && skill.enabled !== false);
|
|
28
|
+
lines.push(`Available ufoo/ucode skills and preset workflows: ${list.length}`);
|
|
29
|
+
for (const skill of list) {
|
|
30
|
+
lines.push(`- ${skill.name}: ${skill.description} (${skill.scope}, ${skill.path})`);
|
|
31
|
+
}
|
|
32
|
+
const errs = Array.isArray(errors) ? errors : [];
|
|
33
|
+
if (errs.length > 0) {
|
|
34
|
+
lines.push("");
|
|
35
|
+
lines.push(`Skill load warnings: ${errs.length}`);
|
|
36
|
+
for (const err of errs) {
|
|
37
|
+
lines.push(`- ${err.path}: ${err.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
renderSkillsSection,
|
|
45
|
+
formatSkillsList,
|
|
46
|
+
};
|
package/src/daemon/index.js
CHANGED
|
@@ -739,6 +739,10 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
|
739
739
|
subscriber: null,
|
|
740
740
|
queueFile: null,
|
|
741
741
|
pending: new Set(),
|
|
742
|
+
watchedAgents: new Set(),
|
|
743
|
+
lastEventSeq: 0,
|
|
744
|
+
emittedEventKeys: [],
|
|
745
|
+
emittedEventKeySet: new Set(),
|
|
742
746
|
};
|
|
743
747
|
const eventBus = new EventBus(projectRoot);
|
|
744
748
|
let joinInProgress = false;
|
|
@@ -758,6 +762,169 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
|
758
762
|
return agentId;
|
|
759
763
|
}
|
|
760
764
|
|
|
765
|
+
function getEventDedupeKey(evt) {
|
|
766
|
+
if (!evt || typeof evt !== "object") return "";
|
|
767
|
+
const seq = Number(evt.seq);
|
|
768
|
+
if (Number.isFinite(seq) && seq > 0) return `seq:${seq}`;
|
|
769
|
+
return [
|
|
770
|
+
"event",
|
|
771
|
+
evt.timestamp || evt.ts || "",
|
|
772
|
+
evt.event || "",
|
|
773
|
+
evt.publisher || "",
|
|
774
|
+
evt.target || "",
|
|
775
|
+
JSON.stringify(evt.data || {}),
|
|
776
|
+
].join(":");
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function rememberEmittedEvent(evt) {
|
|
780
|
+
const key = getEventDedupeKey(evt);
|
|
781
|
+
if (!key) return false;
|
|
782
|
+
if (state.emittedEventKeySet.has(key)) return true;
|
|
783
|
+
state.emittedEventKeySet.add(key);
|
|
784
|
+
state.emittedEventKeys.push(key);
|
|
785
|
+
if (state.emittedEventKeys.length > 500) {
|
|
786
|
+
const removed = state.emittedEventKeys.splice(0, state.emittedEventKeys.length - 500);
|
|
787
|
+
for (const item of removed) state.emittedEventKeySet.delete(item);
|
|
788
|
+
}
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function hasPositiveSeq(seq) {
|
|
793
|
+
const value = Number(seq);
|
|
794
|
+
return Number.isFinite(value) && value > 0;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function toBridgeEvent(evt) {
|
|
798
|
+
const data = evt.data && typeof evt.data === "object" ? evt.data : {};
|
|
799
|
+
return {
|
|
800
|
+
seq: evt.seq,
|
|
801
|
+
event: evt.event,
|
|
802
|
+
publisher: evt.publisher,
|
|
803
|
+
target: evt.target,
|
|
804
|
+
data,
|
|
805
|
+
message: data.message || "",
|
|
806
|
+
state: data.state || "",
|
|
807
|
+
previous: data.previous || "",
|
|
808
|
+
subscriber: data.subscriber || "",
|
|
809
|
+
source: data.source || "",
|
|
810
|
+
injection_mode: data.injection_mode || "",
|
|
811
|
+
ts: evt.timestamp || evt.ts,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function emitBusEvent(evt) {
|
|
816
|
+
if (!evt || !onEvent) return;
|
|
817
|
+
if (rememberEmittedEvent(evt)) return;
|
|
818
|
+
onEvent(toBridgeEvent(evt));
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function readAgentsData() {
|
|
822
|
+
try {
|
|
823
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
824
|
+
return JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
825
|
+
} catch {
|
|
826
|
+
return {};
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function buildWatchedAliases() {
|
|
831
|
+
const aliases = new Set();
|
|
832
|
+
const bus = readAgentsData();
|
|
833
|
+
for (const agentId of state.watchedAgents) {
|
|
834
|
+
aliases.add(agentId);
|
|
835
|
+
const meta = bus.agents && bus.agents[agentId];
|
|
836
|
+
if (!meta) continue;
|
|
837
|
+
if (meta.nickname) aliases.add(meta.nickname);
|
|
838
|
+
if (meta.scoped_nickname) aliases.add(meta.scoped_nickname);
|
|
839
|
+
if (meta.display_nickname) aliases.add(meta.display_nickname);
|
|
840
|
+
}
|
|
841
|
+
return aliases;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function isWatchedEvent(evt, aliases = buildWatchedAliases()) {
|
|
845
|
+
if (!evt || (evt.event !== "message" && evt.event !== "activity_state_changed")) return false;
|
|
846
|
+
const publisher = String(evt.publisher || "");
|
|
847
|
+
const target = String(evt.target || "");
|
|
848
|
+
const subscriber = evt.data && evt.data.subscriber ? String(evt.data.subscriber) : "";
|
|
849
|
+
return aliases.has(publisher) || aliases.has(target) || aliases.has(subscriber);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function getEventFiles() {
|
|
853
|
+
try {
|
|
854
|
+
const dir = getUfooPaths(projectRoot).busEventsDir;
|
|
855
|
+
return fs.readdirSync(dir)
|
|
856
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
857
|
+
.sort()
|
|
858
|
+
.map((name) => path.join(dir, name));
|
|
859
|
+
} catch {
|
|
860
|
+
return [];
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function readCurrentSeq() {
|
|
865
|
+
try {
|
|
866
|
+
const raw = fs.readFileSync(path.join(getUfooPaths(projectRoot).busDir, "seq.counter"), "utf8").trim();
|
|
867
|
+
const seq = Number(raw);
|
|
868
|
+
return Number.isFinite(seq) ? seq : 0;
|
|
869
|
+
} catch {
|
|
870
|
+
return 0;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function readEventFile(file) {
|
|
875
|
+
try {
|
|
876
|
+
return fs.readFileSync(file, "utf8")
|
|
877
|
+
.split(/\r?\n/)
|
|
878
|
+
.filter(Boolean)
|
|
879
|
+
.map((line) => {
|
|
880
|
+
try {
|
|
881
|
+
return JSON.parse(line);
|
|
882
|
+
} catch {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
})
|
|
886
|
+
.filter(Boolean);
|
|
887
|
+
} catch {
|
|
888
|
+
return [];
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function emitRecentWatchedEvents(agentId, limit = 80) {
|
|
893
|
+
if (!agentId) return;
|
|
894
|
+
const previous = new Set(state.watchedAgents);
|
|
895
|
+
state.watchedAgents.add(agentId);
|
|
896
|
+
const aliases = buildWatchedAliases();
|
|
897
|
+
state.watchedAgents = previous;
|
|
898
|
+
const matches = [];
|
|
899
|
+
const files = getEventFiles().slice(-3);
|
|
900
|
+
for (const file of files) {
|
|
901
|
+
for (const evt of readEventFile(file)) {
|
|
902
|
+
if (isWatchedEvent(evt, aliases)) matches.push(evt);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
for (const evt of matches.slice(-limit)) emitBusEvent(evt);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function pollWatchedEvents() {
|
|
909
|
+
if (state.watchedAgents.size === 0) {
|
|
910
|
+
state.lastEventSeq = readCurrentSeq();
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const aliases = buildWatchedAliases();
|
|
914
|
+
let maxSeq = state.lastEventSeq;
|
|
915
|
+
for (const file of getEventFiles().slice(-2)) {
|
|
916
|
+
for (const evt of readEventFile(file)) {
|
|
917
|
+
const seq = Number(evt.seq);
|
|
918
|
+
if (hasPositiveSeq(seq)) {
|
|
919
|
+
if (seq <= state.lastEventSeq) continue;
|
|
920
|
+
if (seq > maxSeq) maxSeq = seq;
|
|
921
|
+
}
|
|
922
|
+
if (isWatchedEvent(evt, aliases)) emitBusEvent(evt);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
state.lastEventSeq = Math.max(state.lastEventSeq, maxSeq);
|
|
926
|
+
}
|
|
927
|
+
|
|
761
928
|
function ensureSubscriber() {
|
|
762
929
|
if (state.subscriber || joinInProgress) return;
|
|
763
930
|
const debugFile = path.join(getUfooPaths(projectRoot).runDir, "bus-join-debug.txt");
|
|
@@ -785,9 +952,7 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
|
785
952
|
})();
|
|
786
953
|
}
|
|
787
954
|
|
|
788
|
-
function
|
|
789
|
-
ensureSubscriber();
|
|
790
|
-
if (typeof shouldDrain === "function" && !shouldDrain()) return;
|
|
955
|
+
function pollQueue() {
|
|
791
956
|
if (!state.queueFile) return;
|
|
792
957
|
if (!fs.existsSync(state.queueFile)) return;
|
|
793
958
|
let content = "";
|
|
@@ -828,15 +993,7 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
|
828
993
|
continue;
|
|
829
994
|
}
|
|
830
995
|
if (!evt) continue;
|
|
831
|
-
|
|
832
|
-
onEvent({
|
|
833
|
-
event: evt.event,
|
|
834
|
-
publisher: evt.publisher,
|
|
835
|
-
target: evt.target,
|
|
836
|
-
message: evt.data?.message || "",
|
|
837
|
-
ts: evt.timestamp || evt.ts,
|
|
838
|
-
});
|
|
839
|
-
}
|
|
996
|
+
emitBusEvent(evt);
|
|
840
997
|
if (evt.publisher && state.pending.has(evt.publisher)) {
|
|
841
998
|
state.pending.delete(evt.publisher);
|
|
842
999
|
if (onStatus) {
|
|
@@ -847,6 +1004,13 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
|
847
1004
|
}
|
|
848
1005
|
}
|
|
849
1006
|
|
|
1007
|
+
function poll() {
|
|
1008
|
+
ensureSubscriber();
|
|
1009
|
+
if (typeof shouldDrain === "function" && !shouldDrain()) return;
|
|
1010
|
+
pollQueue();
|
|
1011
|
+
pollWatchedEvents();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
850
1014
|
const interval = setInterval(poll, 1000);
|
|
851
1015
|
return {
|
|
852
1016
|
markPending(target) {
|
|
@@ -865,6 +1029,19 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
|
865
1029
|
} catch {}
|
|
866
1030
|
return state.subscriber;
|
|
867
1031
|
},
|
|
1032
|
+
watchAgent(agentId, enabled = true) {
|
|
1033
|
+
if (!agentId) return;
|
|
1034
|
+
if (enabled) {
|
|
1035
|
+
emitRecentWatchedEvents(agentId);
|
|
1036
|
+
state.watchedAgents.add(agentId);
|
|
1037
|
+
state.lastEventSeq = Math.max(state.lastEventSeq, readCurrentSeq());
|
|
1038
|
+
} else {
|
|
1039
|
+
state.watchedAgents.delete(agentId);
|
|
1040
|
+
if (state.watchedAgents.size === 0) {
|
|
1041
|
+
state.lastEventSeq = readCurrentSeq();
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
868
1045
|
stop() {
|
|
869
1046
|
clearInterval(interval);
|
|
870
1047
|
},
|
|
@@ -1187,6 +1364,13 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1187
1364
|
}
|
|
1188
1365
|
return;
|
|
1189
1366
|
}
|
|
1367
|
+
if (req.type === IPC_REQUEST_TYPES.BUS_WATCH) {
|
|
1368
|
+
const agentId = String(req.agent_id || "").trim();
|
|
1369
|
+
if (agentId) {
|
|
1370
|
+
busBridge.watchAgent(agentId, req.enabled !== false);
|
|
1371
|
+
}
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1190
1374
|
if (req.type === IPC_REQUEST_TYPES.CRON) {
|
|
1191
1375
|
if (!daemonCronController) {
|
|
1192
1376
|
socket.write(
|