u-foo 2.3.19 → 2.3.21
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 +24 -10
- package/src/chat/commandExecutor.js +3 -3
- package/src/chat/commands.js +3 -3
- package/src/chat/daemonMessageRouter.js +17 -2
- package/src/chat/index.js +25 -1
- 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 +179 -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,16 @@ 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
|
-
const
|
|
308
|
+
const iconWidth = 9;
|
|
309
|
+
const iconGap = " ";
|
|
310
|
+
const iconLine = (icon = "", text = "") => {
|
|
311
|
+
const pad = " ".repeat(Math.max(0, iconWidth - displayWidth(icon)));
|
|
312
|
+
return `${CLAUDE_ORANGE}${icon}${ANSI_RESET}${pad}${iconGap}${text}`;
|
|
313
|
+
};
|
|
303
314
|
const lines = [
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
315
|
+
iconLine(" ▐▛███▜▌", `${product}v${packageVersion}`),
|
|
316
|
+
iconLine("▝▜█████▛▘", detail),
|
|
317
|
+
iconLine(" ▘▘ ▝▝ ", projectPath),
|
|
307
318
|
"",
|
|
308
319
|
];
|
|
309
320
|
if (width < 44) return lines;
|
|
@@ -464,6 +475,7 @@ function createAgentViewController(options = {}) {
|
|
|
464
475
|
function enterAgentView(agentId, options = {}) {
|
|
465
476
|
if (currentView === "agent" && viewingAgent === agentId) return;
|
|
466
477
|
if (currentView === "agent") {
|
|
478
|
+
if (agentViewUsesBus && viewingAgent) sendBusWatch(viewingAgent, false);
|
|
467
479
|
disconnectAgentOutput();
|
|
468
480
|
disconnectAgentInput();
|
|
469
481
|
}
|
|
@@ -488,6 +500,7 @@ function createAgentViewController(options = {}) {
|
|
|
488
500
|
agentInputSuppressUntil = now() + 300;
|
|
489
501
|
agentViewUsesBus = Boolean(options.useBus);
|
|
490
502
|
if (agentViewUsesBus) {
|
|
503
|
+
sendBusWatch(agentId, true);
|
|
491
504
|
resetBusView(agentId);
|
|
492
505
|
renderBusView();
|
|
493
506
|
} else {
|
|
@@ -511,6 +524,7 @@ function createAgentViewController(options = {}) {
|
|
|
511
524
|
|
|
512
525
|
disconnectAgentOutput();
|
|
513
526
|
disconnectAgentInput();
|
|
527
|
+
if (agentViewUsesBus && viewingAgent) sendBusWatch(viewingAgent, false);
|
|
514
528
|
agentViewUsesBus = false;
|
|
515
529
|
agentOutputSuppressed = false;
|
|
516
530
|
agentBarVisible = false;
|
|
@@ -536,11 +550,11 @@ function createAgentViewController(options = {}) {
|
|
|
536
550
|
setDashboardView("agents");
|
|
537
551
|
setSelectedAgentIndex(-1);
|
|
538
552
|
setScreenGrabKeys(false);
|
|
539
|
-
forceScreenRepaint();
|
|
540
553
|
clearTargetAgent();
|
|
541
554
|
renderDashboard();
|
|
542
555
|
focusInput();
|
|
543
556
|
resizeInput();
|
|
557
|
+
forceScreenRepaint();
|
|
544
558
|
try {
|
|
545
559
|
if (screen.program && typeof screen.program.showCursor === "function") {
|
|
546
560
|
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,
|
|
@@ -375,11 +377,24 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
375
377
|
if (data.silent && !streamPayload) return true;
|
|
376
378
|
if (!displayMessage && !streamPayload) return true;
|
|
377
379
|
|
|
380
|
+
const viewingAgent = getViewingAgent();
|
|
381
|
+
const isOwnInternalPrompt =
|
|
382
|
+
data.source === "chat-internal-agent-view" &&
|
|
383
|
+
viewingAgent &&
|
|
384
|
+
data.target === viewingAgent &&
|
|
385
|
+
publisher !== viewingAgent;
|
|
386
|
+
if (
|
|
387
|
+
getCurrentView() === "agent" &&
|
|
388
|
+
isAgentViewUsesBus() &&
|
|
389
|
+
isOwnInternalPrompt
|
|
390
|
+
) {
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
378
393
|
const isAgentViewTarget =
|
|
379
394
|
getCurrentView() === "agent" &&
|
|
380
395
|
isAgentViewUsesBus() &&
|
|
381
|
-
|
|
382
|
-
|
|
396
|
+
viewingAgent &&
|
|
397
|
+
isAgentEventForViewingAgent(data, viewingAgent, publisher);
|
|
383
398
|
|
|
384
399
|
const displayName = resolveAgentDisplayName(publisher);
|
|
385
400
|
|
package/src/chat/index.js
CHANGED
|
@@ -1062,11 +1062,12 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1062
1062
|
: `${prefix}>`;
|
|
1063
1063
|
|
|
1064
1064
|
promptBox.setContent(content);
|
|
1065
|
+
if (!input.parent || !promptBox.parent) return;
|
|
1066
|
+
|
|
1065
1067
|
promptBox.width = content.length + 1; // content + spacer
|
|
1066
1068
|
input.left = promptBox.width;
|
|
1067
1069
|
input.width = `100%-${promptBox.width}`;
|
|
1068
1070
|
|
|
1069
|
-
if (!input.parent || !promptBox.parent) return;
|
|
1070
1071
|
resizeInput();
|
|
1071
1072
|
if (typeof input._updateCursor === "function") {
|
|
1072
1073
|
input._updateCursor();
|
|
@@ -1581,6 +1582,14 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1581
1582
|
source: "chat-internal-agent-view",
|
|
1582
1583
|
});
|
|
1583
1584
|
},
|
|
1585
|
+
sendBusWatch: (agentId, enabled) => {
|
|
1586
|
+
if (!agentId) return;
|
|
1587
|
+
send({
|
|
1588
|
+
type: IPC_REQUEST_TYPES.BUS_WATCH,
|
|
1589
|
+
agent_id: agentId,
|
|
1590
|
+
enabled: enabled !== false,
|
|
1591
|
+
});
|
|
1592
|
+
},
|
|
1584
1593
|
sendResize: (cols, rows) => {
|
|
1585
1594
|
sendResizeWithCapabilities(cols, rows);
|
|
1586
1595
|
},
|
|
@@ -1612,6 +1621,21 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1612
1621
|
getCurrentView: () => getCurrentView(),
|
|
1613
1622
|
isAgentViewUsesBus: () => isAgentViewUsesBus(),
|
|
1614
1623
|
getViewingAgent: () => getViewingAgent(),
|
|
1624
|
+
isAgentEventForViewingAgent: (data, viewingAgent, publisher) => {
|
|
1625
|
+
if (!viewingAgent) return false;
|
|
1626
|
+
const label = getAgentLabel(viewingAgent);
|
|
1627
|
+
const candidates = [
|
|
1628
|
+
publisher,
|
|
1629
|
+
data && data.publisher,
|
|
1630
|
+
data && data.target,
|
|
1631
|
+
data && data.subscriber,
|
|
1632
|
+
].filter(Boolean);
|
|
1633
|
+
return candidates.some((value) => (
|
|
1634
|
+
value === viewingAgent ||
|
|
1635
|
+
value === label ||
|
|
1636
|
+
resolveAgentId(value) === viewingAgent
|
|
1637
|
+
));
|
|
1638
|
+
},
|
|
1615
1639
|
writeToAgentTerm,
|
|
1616
1640
|
consumePendingDelivery,
|
|
1617
1641
|
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,153 @@ 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 pollWatchedEvents() {
|
|
893
|
+
if (state.watchedAgents.size === 0) {
|
|
894
|
+
state.lastEventSeq = readCurrentSeq();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const aliases = buildWatchedAliases();
|
|
898
|
+
let maxSeq = state.lastEventSeq;
|
|
899
|
+
for (const file of getEventFiles().slice(-2)) {
|
|
900
|
+
for (const evt of readEventFile(file)) {
|
|
901
|
+
const seq = Number(evt.seq);
|
|
902
|
+
if (hasPositiveSeq(seq)) {
|
|
903
|
+
if (seq <= state.lastEventSeq) continue;
|
|
904
|
+
if (seq > maxSeq) maxSeq = seq;
|
|
905
|
+
}
|
|
906
|
+
if (isWatchedEvent(evt, aliases)) emitBusEvent(evt);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
state.lastEventSeq = Math.max(state.lastEventSeq, maxSeq);
|
|
910
|
+
}
|
|
911
|
+
|
|
761
912
|
function ensureSubscriber() {
|
|
762
913
|
if (state.subscriber || joinInProgress) return;
|
|
763
914
|
const debugFile = path.join(getUfooPaths(projectRoot).runDir, "bus-join-debug.txt");
|
|
@@ -785,9 +936,7 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
|
785
936
|
})();
|
|
786
937
|
}
|
|
787
938
|
|
|
788
|
-
function
|
|
789
|
-
ensureSubscriber();
|
|
790
|
-
if (typeof shouldDrain === "function" && !shouldDrain()) return;
|
|
939
|
+
function pollQueue() {
|
|
791
940
|
if (!state.queueFile) return;
|
|
792
941
|
if (!fs.existsSync(state.queueFile)) return;
|
|
793
942
|
let content = "";
|
|
@@ -828,15 +977,7 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
|
828
977
|
continue;
|
|
829
978
|
}
|
|
830
979
|
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
|
-
}
|
|
980
|
+
emitBusEvent(evt);
|
|
840
981
|
if (evt.publisher && state.pending.has(evt.publisher)) {
|
|
841
982
|
state.pending.delete(evt.publisher);
|
|
842
983
|
if (onStatus) {
|
|
@@ -847,6 +988,13 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
|
847
988
|
}
|
|
848
989
|
}
|
|
849
990
|
|
|
991
|
+
function poll() {
|
|
992
|
+
ensureSubscriber();
|
|
993
|
+
if (typeof shouldDrain === "function" && !shouldDrain()) return;
|
|
994
|
+
pollQueue();
|
|
995
|
+
pollWatchedEvents();
|
|
996
|
+
}
|
|
997
|
+
|
|
850
998
|
const interval = setInterval(poll, 1000);
|
|
851
999
|
return {
|
|
852
1000
|
markPending(target) {
|
|
@@ -865,6 +1013,18 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
|
|
|
865
1013
|
} catch {}
|
|
866
1014
|
return state.subscriber;
|
|
867
1015
|
},
|
|
1016
|
+
watchAgent(agentId, enabled = true) {
|
|
1017
|
+
if (!agentId) return;
|
|
1018
|
+
if (enabled) {
|
|
1019
|
+
state.watchedAgents.add(agentId);
|
|
1020
|
+
state.lastEventSeq = Math.max(state.lastEventSeq, readCurrentSeq());
|
|
1021
|
+
} else {
|
|
1022
|
+
state.watchedAgents.delete(agentId);
|
|
1023
|
+
if (state.watchedAgents.size === 0) {
|
|
1024
|
+
state.lastEventSeq = readCurrentSeq();
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
},
|
|
868
1028
|
stop() {
|
|
869
1029
|
clearInterval(interval);
|
|
870
1030
|
},
|
|
@@ -1187,6 +1347,13 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1187
1347
|
}
|
|
1188
1348
|
return;
|
|
1189
1349
|
}
|
|
1350
|
+
if (req.type === IPC_REQUEST_TYPES.BUS_WATCH) {
|
|
1351
|
+
const agentId = String(req.agent_id || "").trim();
|
|
1352
|
+
if (agentId) {
|
|
1353
|
+
busBridge.watchAgent(agentId, req.enabled !== false);
|
|
1354
|
+
}
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1190
1357
|
if (req.type === IPC_REQUEST_TYPES.CRON) {
|
|
1191
1358
|
if (!daemonCronController) {
|
|
1192
1359
|
socket.write(
|