@netnodeag/kraftwerk 0.19.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/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
|
|
@@ -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(() => "");
|