@netnodeag/kraftwerk 0.18.0 → 0.20.0
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 +10 -4
- package/dist/cli/kraftwerk.js +3 -0
- package/dist/cli/routines.d.ts +2 -0
- package/dist/cli/routines.js +177 -0
- package/dist/inspector/chat/sessions.js +7 -5
- package/dist/inspector/server.js +36 -1
- package/dist/inspector/skills.d.ts +27 -4
- package/dist/inspector/skills.js +50 -5
- package/inspector/dist/assets/index-D4rWA_E2.js +87 -0
- package/inspector/dist/assets/{index-DIZNVems.css → index-D6fyStMW.css} +1 -1
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/inspector/dist/assets/index-BsQH5ndD.js +0 -81
package/README.md
CHANGED
|
@@ -205,14 +205,20 @@ editing the files — the definition is read fresh for each new session).
|
|
|
205
205
|
|
|
206
206
|
Chats — general ones and team sessions — can use skills: Claude-style
|
|
207
207
|
instruction packages, one folder per skill with a `SKILL.md` (YAML
|
|
208
|
-
frontmatter `name` + `description`, then the instructions).
|
|
209
|
-
|
|
208
|
+
frontmatter `name` + `description`, then the instructions). Discovered
|
|
209
|
+
roots, each shadowing same-named skills in the roots below it:
|
|
210
210
|
|
|
211
211
|
```
|
|
212
|
-
<project
|
|
213
|
-
|
|
212
|
+
<project>/agents/<slug>/skills/<name>/SKILL.md # private to that one team agent
|
|
213
|
+
<project>/<skills root>/<name>/SKILL.md # workspace, git-tracked (kraftwerk.yml `skills`, default skills/)
|
|
214
|
+
<project>/.claude/skills/<name>/SKILL.md # git-tracked, per project
|
|
215
|
+
~/.claude/skills/<name>/SKILL.md # personal, per user
|
|
214
216
|
```
|
|
215
217
|
|
|
218
|
+
Agent skills are visible only to that agent's sessions and always apply —
|
|
219
|
+
a member's `skills:` allowlist narrows the shared roots only. They're
|
|
220
|
+
managed in the agent profile ("own skills") or by editing the files.
|
|
221
|
+
|
|
216
222
|
Every chat lists its visible skills as context ("## Your skills"), so the
|
|
217
223
|
agent reaches for one when the request matches. Typing `/` in the composer
|
|
218
224
|
opens an autocomplete over them; sending `/<name> <args>` expands the
|
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -12,6 +12,7 @@ import { renderCreateBrief } from "./create-brief.js";
|
|
|
12
12
|
import { runDoctor } from "./doctor.js";
|
|
13
13
|
import { runInit } from "./init.js";
|
|
14
14
|
import { registerKnowledgeCommands } from "./knowledge.js";
|
|
15
|
+
import { registerRoutineCommands } from "./routines.js";
|
|
15
16
|
import { listRuns, showRun } from "./runs.js";
|
|
16
17
|
import { runUi } from "./ui.js";
|
|
17
18
|
/**
|
|
@@ -22,6 +23,7 @@ import { runUi } from "./ui.js";
|
|
|
22
23
|
* kraftwerk run [workflow] [text] run one (prompts interactively if omitted)
|
|
23
24
|
* kraftwerk runs [show <id>] inspect past runs from their traces
|
|
24
25
|
* kraftwerk knowledge ... Context & Knowledge: OKF bundles (list/get/put/verify/...)
|
|
26
|
+
* kraftwerk routines ... per-agent scheduled prompts (list/add/remove/enable/run)
|
|
25
27
|
* kraftwerk ui start the inspector web UI (localhost:1981)
|
|
26
28
|
* kraftwerk doctor preflight: harness CLIs, docker, workflows, env
|
|
27
29
|
* kraftwerk validate [paths...] validate without executing
|
|
@@ -253,6 +255,7 @@ runs
|
|
|
253
255
|
await showRun(process.cwd(), runId, opts);
|
|
254
256
|
});
|
|
255
257
|
registerKnowledgeCommands(program);
|
|
258
|
+
registerRoutineCommands(program);
|
|
256
259
|
program
|
|
257
260
|
.command("ui")
|
|
258
261
|
.description("Start the inspector web UI for this project's runs and workflows")
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { resolveProject } from "../config.js";
|
|
5
|
+
import { setOutputDir, setProjectRoot } from "../inspector/context.js";
|
|
6
|
+
import { deleteRoutine, listRoutines, routineStatuses, saveRoutine, } from "../inspector/routines.js";
|
|
7
|
+
import { getMember, listMembers } from "../inspector/team.js";
|
|
8
|
+
/**
|
|
9
|
+
* `kraftwerk routines` — CLI surface over per-agent scheduled prompts
|
|
10
|
+
* (agents/<slug>/routines.yml). Definition CRUD works directly on the
|
|
11
|
+
* files; `run` fires through the running inspector server, because the
|
|
12
|
+
* session (chat) and the scheduler live there.
|
|
13
|
+
*/
|
|
14
|
+
/** Point the inspector context at this project so team/routine paths resolve. */
|
|
15
|
+
async function initContext() {
|
|
16
|
+
const project = await resolveProject(process.cwd());
|
|
17
|
+
setProjectRoot(project.root);
|
|
18
|
+
setOutputDir(project.outputDir);
|
|
19
|
+
return { port: project.config.port ?? 1981 };
|
|
20
|
+
}
|
|
21
|
+
async function requireMember(slug) {
|
|
22
|
+
if (!(await getMember(slug))) {
|
|
23
|
+
console.error(chalk.red(`Agent "${slug}" not found (expected agents/${slug}/agent.yml).`));
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function readStdin() {
|
|
28
|
+
let data = "";
|
|
29
|
+
for await (const chunk of process.stdin)
|
|
30
|
+
data += chunk;
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
/** ISO timestamp → local "YYYY-MM-DD HH:mm" (schedules are server-local time). */
|
|
34
|
+
const fmtTime = (iso) => {
|
|
35
|
+
if (!iso)
|
|
36
|
+
return "—";
|
|
37
|
+
const d = new Date(iso);
|
|
38
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
39
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
40
|
+
};
|
|
41
|
+
function routineLine(r) {
|
|
42
|
+
const state = r.enabled ? chalk.green("on ") : chalk.dim("off");
|
|
43
|
+
const flags = r.lastError ? ` ${chalk.red(`error: ${r.lastError}`)}` : "";
|
|
44
|
+
return (` ${chalk.cyan(r.id.padEnd(24))} ${state} ${r.schedule.padEnd(14)} ` +
|
|
45
|
+
chalk.dim(`next ${fmtTime(r.nextRunAt).padEnd(17)} last ${fmtTime(r.lastRunAt)}`) +
|
|
46
|
+
flags);
|
|
47
|
+
}
|
|
48
|
+
export function registerRoutineCommands(program) {
|
|
49
|
+
const routines = program
|
|
50
|
+
.command("routines")
|
|
51
|
+
.description("Per-agent scheduled prompts (agents/<slug>/routines.yml): list, add, run, ...");
|
|
52
|
+
routines
|
|
53
|
+
.command("list", { isDefault: true })
|
|
54
|
+
.description("List routines — all agents, or one agent with schedule state")
|
|
55
|
+
.argument("[agent]", "Agent slug (folder under agents/)")
|
|
56
|
+
.option("--json", "Machine-readable output")
|
|
57
|
+
.action(async (agent, opts) => {
|
|
58
|
+
await initContext();
|
|
59
|
+
const slugs = agent ? [agent] : (await listMembers()).map((m) => m.slug);
|
|
60
|
+
if (agent)
|
|
61
|
+
await requireMember(agent);
|
|
62
|
+
const all = {};
|
|
63
|
+
for (const slug of slugs)
|
|
64
|
+
all[slug] = await routineStatuses(slug);
|
|
65
|
+
if (opts.json)
|
|
66
|
+
return void console.log(JSON.stringify(all, null, 2));
|
|
67
|
+
let any = false;
|
|
68
|
+
for (const [slug, rs] of Object.entries(all)) {
|
|
69
|
+
if (rs.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
any = true;
|
|
72
|
+
console.log(chalk.bold(slug));
|
|
73
|
+
for (const r of rs)
|
|
74
|
+
console.log(routineLine(r));
|
|
75
|
+
}
|
|
76
|
+
if (!any) {
|
|
77
|
+
console.log(chalk.yellow("No routines yet — add one with `kraftwerk routines add <agent> --name ... --schedule ... --prompt ...`."));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
routines
|
|
81
|
+
.command("add")
|
|
82
|
+
.description("Create or update a routine (same id = update)")
|
|
83
|
+
.argument("<agent>", "Agent slug")
|
|
84
|
+
.requiredOption("--name <name>", 'Display name, e.g. "Morning triage"')
|
|
85
|
+
.requiredOption("--schedule <cron>", '5-field cron ("0 9 * * 1-5") or @hourly/@daily/@weekly/@monthly')
|
|
86
|
+
.option("--prompt <text>", "The prompt to post (or pipe it on stdin / use --file)")
|
|
87
|
+
.option("--file <path>", "Read the prompt from a file")
|
|
88
|
+
.option("--id <id>", "Routine id (default: derived from the name)")
|
|
89
|
+
.option("--disabled", "Create it switched off")
|
|
90
|
+
.action(async (agent, opts) => {
|
|
91
|
+
await initContext();
|
|
92
|
+
await requireMember(agent);
|
|
93
|
+
const prompt = opts.prompt ??
|
|
94
|
+
(opts.file ? await readFile(path.resolve(opts.file), "utf8") : process.stdin.isTTY ? "" : await readStdin());
|
|
95
|
+
if (!prompt.trim()) {
|
|
96
|
+
console.error(chalk.red("No prompt (--prompt, --file, or pipe it on stdin)."));
|
|
97
|
+
process.exit(2);
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const existing = new Set((await listRoutines(agent)).map((r) => r.id));
|
|
101
|
+
const routine = await saveRoutine(agent, {
|
|
102
|
+
...(opts.id ? { id: opts.id } : {}),
|
|
103
|
+
name: opts.name,
|
|
104
|
+
schedule: opts.schedule,
|
|
105
|
+
prompt,
|
|
106
|
+
enabled: !opts.disabled,
|
|
107
|
+
});
|
|
108
|
+
const verb = existing.has(routine.id) ? "updated" : "created";
|
|
109
|
+
console.log(`${chalk.green("✔")} ${verb} ${chalk.cyan(`${agent}/${routine.id}`)} ` +
|
|
110
|
+
chalk.dim(`(${routine.schedule}${routine.enabled ? "" : ", disabled"})`));
|
|
111
|
+
if (routine.enabled) {
|
|
112
|
+
console.log(chalk.dim(" fires from the inspector server — make sure `kraftwerk ui` is running."));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
console.error(chalk.red(err.message));
|
|
117
|
+
process.exit(2);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
routines
|
|
121
|
+
.command("remove")
|
|
122
|
+
.description("Delete a routine")
|
|
123
|
+
.argument("<agent>", "Agent slug")
|
|
124
|
+
.argument("<id>", "Routine id (see `kraftwerk routines <agent>`)")
|
|
125
|
+
.action(async (agent, id) => {
|
|
126
|
+
await initContext();
|
|
127
|
+
await requireMember(agent);
|
|
128
|
+
if (!(await listRoutines(agent)).some((r) => r.id === id)) {
|
|
129
|
+
console.error(chalk.red(`Routine "${agent}/${id}" not found.`));
|
|
130
|
+
process.exit(2);
|
|
131
|
+
}
|
|
132
|
+
await deleteRoutine(agent, id);
|
|
133
|
+
console.log(`${chalk.green("✔")} removed ${chalk.cyan(`${agent}/${id}`)}`);
|
|
134
|
+
});
|
|
135
|
+
for (const [verb, enabled] of [["enable", true], ["disable", false]]) {
|
|
136
|
+
routines
|
|
137
|
+
.command(verb)
|
|
138
|
+
.description(`${verb === "enable" ? "Enable" : "Disable"} a routine`)
|
|
139
|
+
.argument("<agent>", "Agent slug")
|
|
140
|
+
.argument("<id>", "Routine id")
|
|
141
|
+
.action(async (agent, id) => {
|
|
142
|
+
await initContext();
|
|
143
|
+
const routine = (await listRoutines(agent)).find((r) => r.id === id);
|
|
144
|
+
if (!routine) {
|
|
145
|
+
console.error(chalk.red(`Routine "${agent}/${id}" not found.`));
|
|
146
|
+
process.exit(2);
|
|
147
|
+
}
|
|
148
|
+
await saveRoutine(agent, { ...routine, enabled });
|
|
149
|
+
console.log(`${chalk.green("✔")} ${chalk.cyan(`${agent}/${id}`)} ${verb}d`);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
routines
|
|
153
|
+
.command("run")
|
|
154
|
+
.description("Fire a routine now (opens a session via the running `kraftwerk ui` server)")
|
|
155
|
+
.argument("<agent>", "Agent slug")
|
|
156
|
+
.argument("<id>", "Routine id")
|
|
157
|
+
.option("--port <port>", "Inspector port (default: kraftwerk.yml `port`, else 1981)")
|
|
158
|
+
.action(async (agent, id, opts) => {
|
|
159
|
+
const { port } = await initContext();
|
|
160
|
+
const p = opts.port ? Number(opts.port) : port;
|
|
161
|
+
const url = `http://localhost:${p}/api/team/${encodeURIComponent(agent)}/routines/${encodeURIComponent(id)}/run`;
|
|
162
|
+
let body;
|
|
163
|
+
try {
|
|
164
|
+
body = (await (await fetch(url, { method: "POST" })).json());
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
console.error(chalk.red(`Inspector not reachable on localhost:${p} — routines fire through the server; start it with \`kraftwerk ui\`.`));
|
|
168
|
+
process.exit(2);
|
|
169
|
+
}
|
|
170
|
+
if (body.error || !body.chatId) {
|
|
171
|
+
console.error(chalk.red(body.error ?? "run failed"));
|
|
172
|
+
process.exit(2);
|
|
173
|
+
}
|
|
174
|
+
console.log(`${chalk.green("✔")} ${chalk.cyan(`${agent}/${id}`)} fired → session ${body.chatId}\n` +
|
|
175
|
+
chalk.dim(` http://localhost:${p}/#/agents/${encodeURIComponent(agent)}/chat/${body.chatId}`));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -74,18 +74,20 @@ function emit(state, ev) {
|
|
|
74
74
|
/* ---------- skills ---------- */
|
|
75
75
|
/**
|
|
76
76
|
* Skills visible to a chat: every discovered skill (workspace skills root
|
|
77
|
-
* + .claude/skills roots
|
|
78
|
-
*
|
|
77
|
+
* + .claude/skills roots, plus the member's own agents/<slug>/skills for team
|
|
78
|
+
* sessions), narrowed by the team member's allowlist when the chat is a team
|
|
79
|
+
* session (absent allowlist = all, empty = none). The agent's own skills
|
|
80
|
+
* always apply — the allowlist narrows shared skills only.
|
|
79
81
|
*/
|
|
80
82
|
async function availableSkills(scope) {
|
|
81
|
-
const all = await listSkills().catch(() => []);
|
|
82
|
-
if (scope.kind !== "team"
|
|
83
|
+
const all = await listSkills(scope.kind === "team" ? scope.member : undefined).catch(() => []);
|
|
84
|
+
if (scope.kind !== "team")
|
|
83
85
|
return all;
|
|
84
86
|
const member = await getMember(scope.member).catch(() => null);
|
|
85
87
|
if (!member || member.skills === undefined)
|
|
86
88
|
return all;
|
|
87
89
|
const allowed = new Set(member.skills.map((n) => n.toLowerCase()));
|
|
88
|
-
return all.filter((s) => allowed.has(s.name.toLowerCase()));
|
|
90
|
+
return all.filter((s) => s.source === "agent" || allowed.has(s.name.toLowerCase()));
|
|
89
91
|
}
|
|
90
92
|
/** "## Your skills" context block (empty when no skills are visible). */
|
|
91
93
|
function skillsBlock(skills) {
|
package/dist/inspector/server.js
CHANGED
|
@@ -9,7 +9,7 @@ import { dockerStatus, triggerRun, stopRun } from "./runner.js";
|
|
|
9
9
|
import { cancelChat, createChat, deleteChat, disposeAllBackends, getChat, listChats, postMessage, resolvePermission, subscribeChat, } from "./chat/sessions.js";
|
|
10
10
|
import { bundleDetail, conceptDetail, createBundle, knowledgeIndex, putConcept, verifyFromUi, } from "./knowledge.js";
|
|
11
11
|
import { deleteMember, getMember, listMembers, saveMember, teamRoot, } from "./team.js";
|
|
12
|
-
import { getSkill, listSkills, skillsRoot } from "./skills.js";
|
|
12
|
+
import { deleteAgentSkill, getSkill, listAgentSkills, listSkills, readSkill, saveAgentSkill, skillsRoot, } from "./skills.js";
|
|
13
13
|
import { deleteRoutine, routineStatuses, runRoutineNow, saveRoutine, startRoutineScheduler, } from "./routines.js";
|
|
14
14
|
/**
|
|
15
15
|
* The inspector server: a plain node:http server with no dependencies.
|
|
@@ -279,6 +279,41 @@ async function handleApi(req, res, url) {
|
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
|
+
// GET/POST /api/team/:slug/skills — the agent's own skills / create or update one
|
|
283
|
+
if (seg.length === 4 && seg[1] === "team" && seg[3] === "skills") {
|
|
284
|
+
const slug = decodeURIComponent(seg[2]);
|
|
285
|
+
try {
|
|
286
|
+
if (method === "GET")
|
|
287
|
+
return json(res, { skills: await listAgentSkills(slug) });
|
|
288
|
+
if (method === "POST") {
|
|
289
|
+
const body = JSON.parse(await readBody(req));
|
|
290
|
+
return json(res, await saveAgentSkill(slug, String(body.name ?? ""), String(body.content ?? "")));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
return json(res, { error: err.message }, 400);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// GET/DELETE /api/team/:slug/skills/:name — one agent skill with content / delete
|
|
298
|
+
if (seg.length === 5 && seg[1] === "team" && seg[3] === "skills") {
|
|
299
|
+
const slug = decodeURIComponent(seg[2]);
|
|
300
|
+
const name = decodeURIComponent(seg[4]);
|
|
301
|
+
try {
|
|
302
|
+
if (method === "GET") {
|
|
303
|
+
const skill = (await listAgentSkills(slug)).find((s) => s.name.toLowerCase() === name.toLowerCase());
|
|
304
|
+
if (!skill)
|
|
305
|
+
return json(res, { error: "not found" }, 404);
|
|
306
|
+
return json(res, { ...skill, content: await readSkill(skill) });
|
|
307
|
+
}
|
|
308
|
+
if (method === "DELETE") {
|
|
309
|
+
await deleteAgentSkill(slug, name);
|
|
310
|
+
return json(res, { ok: true });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
return json(res, { error: err.message }, 400);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
282
317
|
// GET/POST /api/team/:slug/routines — list (with run state) / upsert
|
|
283
318
|
if (seg.length === 4 && seg[1] === "team" && seg[3] === "routines") {
|
|
284
319
|
const slug = decodeURIComponent(seg[2]);
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
* shadows a same-named project skill, which shadows a same-named global
|
|
13
13
|
* skill. Skills are surfaced to every chat as context (and expanded on
|
|
14
14
|
* /<name> invocation), so they work identically on claude, codex, and pi.
|
|
15
|
+
*
|
|
16
|
+
* On top of the shared roots, each team agent can carry private skills in
|
|
17
|
+
*
|
|
18
|
+
* agents/<slug>/skills/<name>/SKILL.md (agent, git-tracked)
|
|
19
|
+
*
|
|
20
|
+
* Agent skills are visible only to that agent's sessions, always apply
|
|
21
|
+
* (the member's allowlist narrows shared skills only), and shadow
|
|
22
|
+
* same-named shared skills.
|
|
15
23
|
*/
|
|
16
24
|
export interface SkillInfo {
|
|
17
25
|
/** Frontmatter `name`, falling back to the folder name. */
|
|
@@ -21,8 +29,10 @@ export interface SkillInfo {
|
|
|
21
29
|
path: string;
|
|
22
30
|
/** Absolute path to the skill folder (for harnesses that take a dir). */
|
|
23
31
|
dir: string;
|
|
24
|
-
/** workspace = kraftwerk skills root; project = .claude/skills; user = ~/.claude/skills. */
|
|
25
|
-
source: "workspace" | "project" | "user";
|
|
32
|
+
/** workspace = kraftwerk skills root; project = .claude/skills; user = ~/.claude/skills; agent = agents/<slug>/skills. */
|
|
33
|
+
source: "workspace" | "project" | "user" | "agent";
|
|
34
|
+
/** For source "agent": the owning member's slug. */
|
|
35
|
+
agent?: string;
|
|
26
36
|
}
|
|
27
37
|
export interface SkillDetail extends SkillInfo {
|
|
28
38
|
/** Full SKILL.md contents. */
|
|
@@ -32,8 +42,21 @@ export interface SkillDetail extends SkillInfo {
|
|
|
32
42
|
}
|
|
33
43
|
/** Absolute workspace skills root (kraftwerk.yml `skills`, default skills/). */
|
|
34
44
|
export declare function skillsRoot(): Promise<string>;
|
|
35
|
-
/**
|
|
36
|
-
export declare function
|
|
45
|
+
/** The private skills root of one team agent: agents/<slug>/skills. */
|
|
46
|
+
export declare function agentSkillsRoot(slug: string): Promise<string>;
|
|
47
|
+
/** One agent's private skills (agents/<slug>/skills/<name>/SKILL.md). */
|
|
48
|
+
export declare function listAgentSkills(slug: string): Promise<SkillInfo[]>;
|
|
49
|
+
/**
|
|
50
|
+
* All discovered skills; agent (when a member is given) shadows workspace
|
|
51
|
+
* shadows project shadows user (global).
|
|
52
|
+
*/
|
|
53
|
+
export declare function listSkills(member?: string): Promise<SkillInfo[]>;
|
|
54
|
+
/** Create or update one agent skill; writes agents/<slug>/skills/<name>/SKILL.md. */
|
|
55
|
+
export declare function saveAgentSkill(slug: string, name: string, content: string): Promise<SkillInfo & {
|
|
56
|
+
content: string;
|
|
57
|
+
}>;
|
|
58
|
+
/** Delete one agent skill (removes its folder). */
|
|
59
|
+
export declare function deleteAgentSkill(slug: string, name: string): Promise<void>;
|
|
37
60
|
/** Read a skill's SKILL.md contents (empty string when unreadable). */
|
|
38
61
|
export declare function readSkill(skill: SkillInfo): Promise<string>;
|
|
39
62
|
/** One skill by name (post-shadowing), with SKILL.md content + bundled files. */
|
package/dist/inspector/skills.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
import { resolveProject } from "../config.js";
|
|
6
6
|
import { getProjectRoot } from "./context.js";
|
|
7
|
+
import { safeMemberSlug, teamRoot } from "./team.js";
|
|
7
8
|
const NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
8
9
|
function frontmatter(md) {
|
|
9
10
|
const m = /^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/.exec(md);
|
|
@@ -57,16 +58,29 @@ async function scanRoot(root, source) {
|
|
|
57
58
|
}));
|
|
58
59
|
return skills.filter(Boolean).sort((a, b) => a.name.localeCompare(b.name));
|
|
59
60
|
}
|
|
60
|
-
/**
|
|
61
|
-
export async function
|
|
62
|
-
|
|
61
|
+
/** The private skills root of one team agent: agents/<slug>/skills. */
|
|
62
|
+
export async function agentSkillsRoot(slug) {
|
|
63
|
+
return path.join(await teamRoot(), safeMemberSlug(slug), "skills");
|
|
64
|
+
}
|
|
65
|
+
/** One agent's private skills (agents/<slug>/skills/<name>/SKILL.md). */
|
|
66
|
+
export async function listAgentSkills(slug) {
|
|
67
|
+
const skills = await scanRoot(await agentSkillsRoot(slug), "agent");
|
|
68
|
+
return skills.map((s) => ({ ...s, agent: slug }));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* All discovered skills; agent (when a member is given) shadows workspace
|
|
72
|
+
* shadows project shadows user (global).
|
|
73
|
+
*/
|
|
74
|
+
export async function listSkills(member) {
|
|
75
|
+
const [agent, workspace, project, user] = await Promise.all([
|
|
76
|
+
member ? listAgentSkills(member).catch(() => []) : [],
|
|
63
77
|
skillsRoot().then((root) => scanRoot(root, "workspace")),
|
|
64
78
|
scanRoot(path.join(getProjectRoot(), ".claude", "skills"), "project"),
|
|
65
79
|
scanRoot(path.join(os.homedir(), ".claude", "skills"), "user"),
|
|
66
80
|
]);
|
|
67
|
-
const out = [...
|
|
81
|
+
const out = [...agent];
|
|
68
82
|
const seen = new Set(out.map((s) => s.name.toLowerCase()));
|
|
69
|
-
for (const s of [...project, ...user]) {
|
|
83
|
+
for (const s of [...workspace, ...project, ...user]) {
|
|
70
84
|
if (seen.has(s.name.toLowerCase()))
|
|
71
85
|
continue;
|
|
72
86
|
seen.add(s.name.toLowerCase());
|
|
@@ -74,6 +88,37 @@ export async function listSkills() {
|
|
|
74
88
|
}
|
|
75
89
|
return out;
|
|
76
90
|
}
|
|
91
|
+
/** Create or update one agent skill; writes agents/<slug>/skills/<name>/SKILL.md. */
|
|
92
|
+
export async function saveAgentSkill(slug, name, content) {
|
|
93
|
+
const n = name.trim().toLowerCase();
|
|
94
|
+
if (!NAME_RE.test(n)) {
|
|
95
|
+
throw new Error("skill name must be letters, digits, - or _ (e.g. report-html)");
|
|
96
|
+
}
|
|
97
|
+
if (!content.trim())
|
|
98
|
+
throw new Error("content (SKILL.md) is required");
|
|
99
|
+
const dir = path.join(await agentSkillsRoot(slug), n);
|
|
100
|
+
await fs.mkdir(dir, { recursive: true });
|
|
101
|
+
const file = path.join(dir, "SKILL.md");
|
|
102
|
+
await fs.writeFile(file, content.endsWith("\n") ? content : content + "\n");
|
|
103
|
+
const fm = frontmatter(content);
|
|
104
|
+
const display = String(fm.name ?? n).trim() || n;
|
|
105
|
+
return {
|
|
106
|
+
name: NAME_RE.test(display) ? display : n,
|
|
107
|
+
description: String(fm.description ?? "").trim(),
|
|
108
|
+
path: file,
|
|
109
|
+
dir,
|
|
110
|
+
source: "agent",
|
|
111
|
+
agent: slug,
|
|
112
|
+
content,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Delete one agent skill (removes its folder). */
|
|
116
|
+
export async function deleteAgentSkill(slug, name) {
|
|
117
|
+
const skill = (await listAgentSkills(slug)).find((s) => s.name.toLowerCase() === name.toLowerCase());
|
|
118
|
+
if (!skill)
|
|
119
|
+
throw new Error(`skill "${name}" not found`);
|
|
120
|
+
await fs.rm(skill.dir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
77
122
|
/** Read a skill's SKILL.md contents (empty string when unreadable). */
|
|
78
123
|
export function readSkill(skill) {
|
|
79
124
|
return fs.readFile(skill.path, "utf8").catch(() => "");
|